Repository: opencis/opencis-core
Pull request #203: Centralize asyncio servers and ask kernel for ephemeral port when run…
Author: login: arter97
Created: 2025-04-04T06:13:43Z | Merged: 2025-04-05T04:03:05Z

…ning tests

Helper component, ServerComponent, was added to opencis/util/server.py that wraps asyncio server for ease of use.

All asyncio.create_task(server.serve_forever()) from opencis/ (ShortMsgConn, SwitchConnectionManager, MctpConnectionManager) now uses it.

Additionally, all server code now supports taking 0 as the port number to allow the kernel to assign an unused ephemeral port.

Dependent client classes now have set_port(int) for assigning said port number from the server class, which also now have get_port() helper function.

This fixes tests failure due to duplicate port numbers once and for all and even allows concurrent pytests.


Diff:
diff --git a/opencis/apps/cxl_switch.py b/opencis/apps/cxl_switch.py
index 4bb24398..adba23dd 100644
--- a/opencis/apps/cxl_switch.py
+++ b/opencis/apps/cxl_switch.py
@@ -123,6 +123,9 @@ def __init__(
 
         self._run_as_child = switch_config.run_as_child
 
+    def get_port(self):
+        return self._switch_connection_manager.get_port()
+
     def _initialize_mctp_endpoint(self):
         ident_payload = IdentifyResponsePayload(
             device_id=SW_SWITCH_DID, component_type=IdentifyComponentType.SWITCH
diff --git a/opencis/apps/fabric_manager.py b/opencis/apps/fabric_manager.py
index ba351074..9c3c2f25 100644
--- a/opencis/apps/fabric_manager.py
+++ b/opencis/apps/fabric_manager.py
@@ -52,6 +52,9 @@ def __init__(
         self._host_fm_conn_server.register_general_handler(HostFMMsg.CONFIRM, self._host_callback())
         self._use_test_runner = use_test_runner
 
+    def get_host_fm_port(self):
+        return self._host_fm_conn_server.get_port()
+
     def _host_callback(self):
         async def _func(_: int, data: HostFMMsg):
             print(f"Received {data.readable} from host (root port={data.root_port})")
diff --git a/opencis/apps/memory_pooling.py b/opencis/apps/memory_pooling.py
index 5547e30e..aee7d537 100644
--- a/opencis/apps/memory_pooling.py
+++ b/opencis/apps/memory_pooling.py
@@ -74,7 +74,7 @@ class MemoryBaseTracker:
 host_fm_conn = None
 
 
-async def my_sys_sw_app(ig: int = None, iw: int = None, **kwargs):
+async def my_sys_sw_app(ig: int = None, iw: int = None, host_fm_conn_port: int = 8700, **kwargs):
     cxl_memory_hub: CxlMemoryHub
 
     # Max addr for CFG is 0x9FFFFFFF, given max num bus = 8
@@ -90,7 +90,12 @@ async def my_sys_sw_app(ig: int = None, iw: int = None, **kwargs):
     root_complex = cxl_memory_hub.get_root_complex()
     root_port = cxl_memory_hub.get_root_port()
     host_fm_conn_client = ShortMsgConn(
-        "FM_Client", port=8700, server=False, msg_width=16, msg_type=HostFMMsg, device_id=root_port
+        "FM_Client",
+        port=host_fm_conn_port,
+        server=False,
+        msg_width=16,
+        msg_type=HostFMMsg,
+        device_id=root_port,
     )
     # pylint: disable=global-statement
     global host_fm_conn  # To prevent the connection from GC'ed after function's done
diff --git a/opencis/cxl/component/host_manager.py b/opencis/cxl/component/host_manager.py
index d03e3e9e..799e06f3 100644
--- a/opencis/cxl/component/host_manager.py
+++ b/opencis/cxl/component/host_manager.py
@@ -43,6 +43,9 @@ def __init__(
             "HOST_INIT": self._host_init,
         }
 
+    def get_port(self):
+        return self._port
+
     async def _host_init(self, port: int) -> jsonrpcserver.Result:
         logger.info(self._create_message(f"Connection opened by CxlHost:Port{port}"))
         return jsonrpcserver.Success({"port": port})
@@ -58,6 +61,8 @@ async def _serve(self, ws: WebSocketClientProtocol):
     async def serve(self):
         self._fut = asyncio.Future()
         self._host_server = await websockets.serve(self._serve, self._host, self._port)
+        if self._port == 0:
+            self._port = self._host_server.sockets[0].getsockname()[1]
         await self._change_status_to_running()
         res = await self._fut
         logger.debug(self._create_message(f"{res}"))
@@ -152,6 +157,9 @@ def __init__(
         self._util_server = None
         self._get_host_conn_callback = get_host_conn_callback
 
+    def get_port(self):
+        return self._port
+
     async def _process_cmd(self, cmd: str, port: int) -> jsonrpcserver.Result:
         ws = await self._get_host_conn_callback(port)
         if ws is None:
@@ -184,6 +192,8 @@ async def _serve(self, ws):
     async def serve(self):
         self._fut = asyncio.Future()
         self._util_server = await websockets.serve(self._serve, self._host, self._port)
+        if self._port == 0:
+            self._port = self._util_server.sockets[0].getsockname()[1]
         await self._change_status_to_running()
         res = await self._fut
         logger.debug(self._create_message(f"{res}"))
@@ -243,6 +253,12 @@ def __init__(
         )
         self._util_conn_server = UtilConnServer(util_host, util_port, self._get_host_conn_callback)
 
+    def get_host_port(self):
+        return self._host_conn_server.get_port()
+
+    def get_util_port(self):
+        return self._util_conn_server.get_port()
+
     async def _set_host_conn_callback(self, port: int, ws) -> WebSocketClientProtocol:
         self._host_connections[port] = ws
 
diff --git a/opencis/cxl/component/mctp/mctp_connection_manager.py b/opencis/cxl/component/mctp/mctp_connection_manager.py
index 0f124379..82d55867 100644
--- a/opencis/cxl/component/mctp/mctp_connection_manager.py
+++ b/opencis/cxl/component/mctp/mctp_connection_manager.py
@@ -16,6 +16,7 @@
     MCTP_PACKET_PROCESSOR_TYPE,
 )
 from opencis.util.component import RunnableComponent
+from opencis.util.server import ServerComponent
 
 # pylint: disable=duplicate-code
 
@@ -40,57 +41,43 @@ def __init__(
         self._connection_timeout_ms = connection_timeout_ms
         # TODO: Support receiving connections from CXL Devices
         self._switch_port = MctpPort()
-        self._server_task = None
+        self._server_component = ServerComponent(
+            handle_client=self._handle_client,
+            host=self._host,
+            port=self._port,
+            stop_callback=self._stop_callback,
+        )
 
     async def _run(self):
-        # pylint: disable=bare-except
-        try:
-            logger.info(self._create_message(f"Creating TCP server at port {self._port}"))
-            server = await self._create_server()
-            self._server_task = asyncio.create_task(server.serve_forever())
-            logger.info(self._create_message("Starting TCP server task"))
-            while not server.is_serving():
-                await asyncio.sleep(0.1)
-            await self._change_status_to_running()
-            await self._server_task
-        except Exception as e:
-            logger.debug(self._create_message(f"Exception: {str(e)}"))
-        except:
-            logger.info(self._create_message("Stopped TCP server"))
-            if self._switch_port.packet_processor is not None:
-                logger.info(self._create_message("Stopping PacketProcessor for Switch Port"))
-                await self._switch_port.packet_processor.stop()
-                logger.info(self._create_message("Stopped PacketProcessor for Switch Port"))
+        server_task = asyncio.create_task(self._server_component.run())
+        await self._server_component.wait_for_ready()
+        self._port = self._server_component.get_port()
+        await self._change_status_to_running()
+        await server_task
+
+    async def _stop_callback(self):
+        if self._switch_port.packet_processor is not None:
+            logger.info(self._create_message("Stopping PacketProcessor for Switch Port"))
+            await self._switch_port.packet_processor.stop()
+            logger.info(self._create_message("Stopped PacketProcessor for Switch Port"))
 
     async def _stop(self):
         logger.info(self._create_message("Canceling TCP server task"))
-        self._server_task.cancel()
-
-    async def _create_server(self):
-        async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
-            try:
-                logger.info(self._create_message("Found a new socket connection"))
-                if self._switch_port.connected:
-                    logger.warning(
-                        self._create_message("Connection already exists for Switch Port")
-                    )
-                else:
-                    logger.info(self._create_message("Binding incoming connection to Switch Port"))
-                    self._switch_port.connected = True
-                    await self._start_packet_processor(reader, writer)
-            except Exception as e:
-                logger.warning(self._create_message(str(e)))
+        await self._server_component.stop()
 
-            self._switch_port.connected = False
-            await self._close_connection(writer)
-
-        server = await asyncio.start_server(handle_client, self._host, self._port)
-        return server
+    async def _handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
+        try:
+            logger.info(self._create_message("Found a new socket connection"))
+            if self._switch_port.connected:
+                logger.warning(self._create_message("Connection already exists for Switch Port"))
+            else:
+                logger.info(self._create_message("Binding incoming connection to Switch Port"))
+                self._switch_port.connected = True
+                await self._start_packet_processor(reader, writer)
+        except Exception as e:
+            logger.warning(self._create_message(str(e)))
 
-    async def _close_connection(self, writer: asyncio.StreamWriter):
-        writer.close()
-        await writer.wait_closed()
-        logger.info(self._create_message("Closed connnection"))
+        self._switch_port.connected = False
 
     async def _start_packet_processor(
         self,
diff --git a/opencis/cxl/component/short_msg_conn.py b/opencis/cxl/component/short_msg_conn.py
index 88991afa..57b940dc 100644
--- a/opencis/cxl/component/short_msg_conn.py
+++ b/opencis/cxl/component/short_msg_conn.py
@@ -12,17 +12,16 @@
     Task,
     create_task,
     gather,
-    start_server,
     open_connection,
     Lock,
 )
-import asyncio
 from asyncio.exceptions import CancelledError
 from enum import Enum
 from typing import Callable
 
 from opencis.util.component import RunnableComponent
 from opencis.util.logger import logger
+from opencis.util.server import ServerComponent
 
 
 class ShortMsgBase(Enum):
@@ -34,7 +33,7 @@ def real_val(self):
 class ShortMsgConn(RunnableComponent):
     _msg_to_interrupt_event: dict[int, dict[ShortMsgBase, Callable]]
     _callbacks: list[Callable]
-    _server_task: Task
+    _server_component: Task
 
     def __init__(
         self,
@@ -54,6 +53,14 @@ def __init__(
         self._msg_to_interrupt_event = {}
         self._general_interrupt_event = {}
         self._server = server
+        if server:
+            self._server_component = ServerComponent(
+                handle_client=self._new_conn,
+                host=self._addr,
+                port=self._port,
+            )
+        else:
+            self._server_component = None
         self._connections: dict[int, tuple[StreamReader, StreamWriter]] = {}
         self._tasks: list[Task] = []
         self._msg_handlers: list[Task] = []
@@ -66,6 +73,9 @@ def __init__(
         self._msg_tasks: list[Task] = []
         self._msg_type = msg_type
 
+    def get_port(self):
+        return self._port
+
     def register_interrupt_handler(
         self, short_msg: ShortMsgBase, msg_recv_cb: Callable, dev_id: int = 0
     ):
@@ -156,18 +166,14 @@ async def _msg_handler(self, reader: StreamReader, _: StreamWriter):
                 )
             )
 
-    async def _create_server(self):
-        self._run_status = True
-
-        async def _new_conn(reader: StreamReader, writer: StreamWriter):
-            remote_dev_id = await reader.readexactly(16)
-            remote_dev_id_int = int.from_bytes(remote_dev_id, "little")
-            self._connections[remote_dev_id_int] = (reader, writer)
-            self._msg_handlers.append(create_task(self._msg_handler(reader, writer)))
-
-        server = await start_server(_new_conn, self._addr, self._port, limit=2)
-        logger.debug(self._create_message(f"Starting ShortMsg server on {self._addr}:{self._port}"))
-        return server
+    async def _new_conn(self, reader: StreamReader, writer: StreamWriter):
+        logger.debug(
+            self._create_message(f"New ShortMsg connection: {writer.get_extra_info('peername')}")
+        )
+        remote_dev_id = await reader.readexactly(16)
+        remote_dev_id_int = int.from_bytes(remote_dev_id, "little")
+        self._connections[remote_dev_id_int] = (reader, writer)
+        self._msg_handlers.append(create_task(self._msg_handler(reader, writer)))
 
     async def send_irq_request(self, request: ShortMsgBase, device: int = 0):
         """
@@ -200,11 +206,11 @@ async def shutdown(self):
     async def _run(self):
         try:
             if self._server:
-                server = await self._create_server()
-                self._server_task = create_task(server.serve_forever())
-                while not server.is_serving():
-                    await asyncio.sleep(0.1)
-                self._tasks.append(self._server_task)
+                server_task = create_task(self._server_component.run())
+                await self._server_component.wait_for_ready()
+                self._port = self._server_component.get_port()
+                self._run_status = True
+                self._tasks.append(server_task)
             else:
                 pass
             await self._change_status_to_running()
diff --git a/opencis/cxl/component/switch_connection_client.py b/opencis/cxl/component/switch_connection_client.py
index 1d186f0e..10c46a93 100644
--- a/opencis/cxl/component/switch_connection_client.py
+++ b/opencis/cxl/component/switch_connection_client.py
@@ -98,6 +98,9 @@ def get_cxl_connection(self):
     def get_port_index(self):
         return self._port_index
 
+    def set_port(self, port: int):
+        self._port = port
+
     async def _run(self):
         if self._retry:
             time_out = 120
diff --git a/opencis/cxl/component/switch_connection_manager.py b/opencis/cxl/component/switch_connection_manager.py
index 06dc032c..6aeaabe7 100644
--- a/opencis/cxl/component/switch_connection_manager.py
+++ b/opencis/cxl/component/switch_connection_manager.py
@@ -6,7 +6,7 @@
 """
 
 import asyncio
-from asyncio import CancelledError, create_task, gather
+from asyncio import create_task, gather
 from dataclasses import dataclass, field
 from enum import Enum, auto
 from typing import Optional, List, Callable, Coroutine, Any, cast
@@ -29,6 +29,7 @@
 from opencis.cxl.component.common import CXL_COMPONENT_TYPE
 from opencis.util.component import RunnableComponent
 from opencis.util.logger import logger
+from opencis.util.server import ServerComponent
 
 
 @dataclass
@@ -68,85 +69,35 @@ def __init__(
         self._port = port
         self._connection_timeout_ms = connection_timeout_ms
         self._ports = [SwitchPort(port_config=port_config) for port_config in port_configs]
-        self._server_task = None
+        self._server_component = ServerComponent(
+            handle_client=self._handle_client,
+            host=self._host,
+            port=self._port,
+        )
         self._event_handler = None
-        self._clients = set()
 
-    async def _run(self):
+    async def _handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
+        port_index = None
         try:
-            logger.info(self._create_message("Creating TCP server"))
-            server = await self._create_server()
-            self._server_task = asyncio.create_task(server.serve_forever())
-            logger.info(self._create_message("Starting TCP server task"))
-            while not server.is_serving():
-                await asyncio.sleep(0.1)
-            await self._change_status_to_running()
-            await self._server_task
+            logger.info(self._create_message("Found a new socket connection"))
+            port_index = await self._wait_for_connection_request(reader)
+            await self._send_confirmation(writer)
+            logger.info(self._create_message(f"Binding incoming connection to port {port_index}"))
+            await self._update_connection_status(port_index, connected=True)
+            await self._start_packet_processor(reader, writer, port_index)
         except Exception as e:
             logger.error(
                 self._create_message(
                     f"{self.__class__.__name__} error: {str(e)}, {traceback.format_exc()}"
                 )
             )
-        except CancelledError:
-            logger.info(self._create_message("Stopped TCP server"))
-
-    async def _stop(self):
-        logger.info(self._create_message("Cancelling TCP server task"))
-        self._server_task.cancel()
-        for client in self._clients.copy():
-            logger.info(
-                self._create_message(
-                    f'Closing client connection: {client.get_extra_info("peername")}'
-                )
-            )
-            client.close()
-        for client in self._clients.copy():
-            await client.wait_closed()
-            logger.info(
-                self._create_message(
-                    f'Closed client connection: {client.get_extra_info("peername")}'
-                )
-            )
-        self._clients.clear()
-        try:
-            await self._server_task
-        except CancelledError:
-            logger.info(self._create_message("Cancelled TCP server"))
-
-    async def _create_server(self):
-        async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
-            self._clients.add(writer)
-            port_index = None
-            try:
-                logger.info(
-                    self._create_message(
-                        f"Found a new socket connection: {writer.get_extra_info('peername')}"
-                    )
-                )
-                port_index = await self._wait_for_connection_request(reader)
-                await self._send_confirmation(writer)
-                logger.info(
-                    self._create_message(f"Binding incoming connection to port {port_index}")
-                )
-                await self._update_connection_status(port_index, connected=True)
-                await self._start_packet_processor(reader, writer, port_index)
-            except Exception as e:
-                logger.error(
-                    self._create_message(
-                        f"{self.__class__.__name__} error: {str(e)}, {traceback.format_exc()}"
-                    )
-                )
 
-            if port_index is None:
-                await self._send_rejection(writer)
-            else:
-                await self._update_connection_status(port_index, connected=False)
-            await self._close_connection(writer, port_index)
-
-        logger.info(self._create_message(f"Listening to port {self._port}"))
-        server = await asyncio.start_server(handle_client, self._host, self._port)
-        return server
+        if port_index is None:
+            await self._send_rejection(writer)
+            # Connection closed log printed from ServerComponent
+        else:
+            await self._update_connection_status(port_index, connected=False)
+            logger.info(self._create_message(f"Closed client connection for port {port_index}"))
 
     async def _update_connection_status(self, port_id: int, connected: bool):
         self._ports[port_id].connected = connected
@@ -154,31 +105,6 @@ async def _update_connection_status(self, port_id: int, connected: bool):
             return
         await self._event_handler(PortUpdateEvent(port_id=port_id, connected=connected))
 
-    async def _close_connection(
-        self, writer: asyncio.StreamWriter, port_index: Optional[int] = None
-    ):
-        description = writer.get_extra_info("peername")
-        self._clients.discard(writer)
-        writer.close()
-        try:
-            await writer.wait_closed()
-        except Exception as e:
-            logger.error(
-                self._create_message(
-                    f"{self.__class__.__name__} error while closing {description}: "
-                    f"{str(e)}, {traceback.format_exc()}"
-                )
-            )
-
-        if port_index is None:
-            logger.info(self._create_message(f"Closed client connection: {description}"))
-        else:
-            logger.info(
-                self._create_message(
-                    f"Closed client connection {description} for port {port_index}"
-                )
-            )
-
     async def _send_confirmation(self, writer: asyncio.StreamWriter):
         sideband_response = BaseSidebandPacket.create(SIDEBAND_TYPES.CONNECTION_ACCEPT)
         writer.write(bytes(sideband_response))
@@ -261,3 +187,16 @@ def get_switch_ports(self) -> List[SwitchPort]:
 
     def register_event_handler(self, event_handler: AsyncEventHandlerType):
         self._event_handler = event_handler
+
+    def get_port(self):
+        return self._port
+
+    async def _run(self):
+        server_task = create_task(self._server_component.run())
+        await self._server_component.wait_for_ready()
+        self._port = self._server_component.get_port()
+        await self._change_status_to_running()
+        await server_task
+
+    async def _stop(self):
+        await self._server_component.stop()
diff --git a/opencis/cxl/component/virtual_switch/virtual_switch.py b/opencis/cxl/component/virtual_switch/virtual_switch.py
index ca787741..cd7cb83c 100644
--- a/opencis/cxl/component/virtual_switch/virtual_switch.py
+++ b/opencis/cxl/component/virtual_switch/virtual_switch.py
@@ -296,5 +296,8 @@ def get_bound_port_id(self, vppb_id: int) -> int:
     def get_ld_id(self, vppb_id: int) -> int:
         return self._vppb_ld_id_map[vppb_id]
 
+    def get_irq_port(self):
+        return self._irq_manager.get_port()
+
     def register_event_handler(self, event_handler: AsyncEventHandlerType):
         self._event_handler = event_handler
diff --git a/opencis/cxl/component/virtual_switch_manager.py b/opencis/cxl/component/virtual_switch_manager.py
index 6a239451..90559710 100644
--- a/opencis/cxl/component/virtual_switch_manager.py
+++ b/opencis/cxl/component/virtual_switch_manager.py
@@ -73,6 +73,13 @@ def get_total_bound_vppbs_count(self) -> int:
             total_bound_vppbs += virtual_switch.get_bound_vppb_counts()
         return total_bound_vppbs
 
+    def get_port(self, switch_index):
+        return self._virtual_switches[switch_index].get_irq_port()
+
+    def register_event_handler(self, event_handler: AsyncEventHandlerType):
+        for vcs in self._virtual_switches:
+            vcs.register_event_handler(event_handler)
+
     async def _run(self):
         run_tasks = []
         for virtual_switch in self._virtual_switches:
@@ -89,7 +96,3 @@ async def _stop(self):
         for virtual_switch in self._virtual_switches:
             tasks.append(create_task(virtual_switch.stop()))
         await gather(*tasks)
-
-    def register_event_handler(self, event_handler: AsyncEventHandlerType):
-        for vcs in self._virtual_switches:
-            vcs.register_event_handler(event_handler)
diff --git a/opencis/util/server.py b/opencis/util/server.py
new file mode 100644
index 00000000..463736fc
--- /dev/null
+++ b/opencis/util/server.py
@@ -0,0 +1,121 @@
+"""
+Copyright (c) 2025, Eeum, Inc.
+
+This software is licensed under the terms of the Revised BSD License.
+See LICENSE for details.
+"""
+
+import asyncio
+from asyncio import CancelledError
+import sys
+from typing import Callable
+import traceback
+
+from opencis.util.component import RunnableComponent
+from opencis.util.logger import logger
+
+
+class ServerComponent(RunnableComponent):
+    def __init__(
+        self,
+        handle_client: Callable,
+        host: str = "0.0.0.0",
+        port: int = 0,
+        stop_callback: Callable = None,
+        label: str = None,
+    ):
+        if label is None:
+            # Get caller function name
+            # pylint: disable=protected-access
+            label = sys._getframe(1).f_locals["self"].__class__.__name__
+        super().__init__(label)
+
+        if handle_client is None:
+            raise ValueError("handle_client must be provided")
+
+        self._host = host
+        self._port = port
+        self._handle_client = handle_client
+        self._stop_callback = stop_callback
+        self._descriptor = f"TCP server ({label})"
+        self._server_task = None
+        self._clients = set()
+
+    def get_port(self):
+        return self._port
+
+    async def _create_server(self):
+        async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
+            self._clients.add(writer)
+            logger.info(
+                self._create_message(
+                    f"Found a new socket connection: {writer.get_extra_info('peername')}"
+                )
+            )
+            await self._handle_client(reader, writer)
+            # Handled, now close the connection
+            self._clients.discard(writer)
+            description = writer.get_extra_info("peername")
+            writer.close()
+            try:
+                await writer.wait_closed()
+            except Exception as e:
+                logger.error(
+                    self._create_message(
+                        f"Error while closing {description}: {str(e)}, {traceback.format_exc()}"
+                    )
+                )
+            logger.info(self._create_message(f"Closed client connection: {description}"))
+
+        logger.info(self._create_message(f"Starting {self._descriptor} server"))
+        server = await asyncio.start_server(handle_client, self._host, self._port)
+        if self._port == 0:
+            # Ephemeral port is chosen by the OS
+            self._port = server.sockets[0].getsockname()[1]
+        logger.info(
+            self._create_message(f"{self._descriptor} listening on {self._host}:{self._port}")
+        )
+        return server
+
+    async def _run(self):
+        try:
+            logger.info(self._create_message(f"Creating {self._descriptor}"))
+            server = await self._create_server()
+            self._server_task = asyncio.create_task(server.serve_forever())
+            while not server.is_serving():
+                await asyncio.sleep(0.1)
+            await self._change_status_to_running()
+            await self._server_task
+        except Exception as e:
+            logger.error(
+                self._create_message(
+                    f"{self._descriptor} error: {str(e)}, {traceback.format_exc()}"
+                )
+            )
+        except CancelledError:
+            logger.info(self._create_message(f"Stopped {self._descriptor}"))
+            if self._stop_callback is not None:
+                await self._stop_callback()
+
+    async def _stop(self):
+        logger.info(self._create_message(f"Cancelling {self._descriptor} task"))
+        self._server_task.cancel()
+        for client in self._clients.copy():
+            logger.info(
+                self._create_message(
+                    f'Closing client connection: {client.get_extra_info("peername")}'
+                )
+            )
+            client.close()
+        for client in self._clients.copy():
+            await client.wait_closed()
+            logger.info(
+                self._create_message(
+                    f'Closed client connection: {client.get_extra_info("peername")}'
+                )
+            )
+        self._clients.clear()
+        try:
+            await self._server_task
+        except CancelledError:
+            logger.info(self._create_message(f"Cancelled {self._descriptor}"))
diff --git a/tests/conftest.py b/tests/conftest.py
index abf610c9..ca07f251 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,7 +5,6 @@
 See LICENSE for details.
 """
 
-from enum import IntEnum, auto
 import pytest
 
 
@@ -20,24 +19,3 @@ def _get_gold_std_reg_vals(device_type: str):
         return None
 
     return _get_gold_std_reg_vals
-
-
-class TEST_PORT(IntEnum):
-    TEST_1 = auto()
-    TEST_2 = auto()
-    TEST_3 = auto()
-    TEST_4 = auto()
-    TEST_5 = auto()
-    TEST_6 = auto()
-    TEST_7 = auto()
-    TEST_8 = auto()
-    TEST_9 = auto()
-    TEST_10 = auto()
-    TEST_11 = auto()
-    TEST_12 = auto()
-    TEST_13 = auto()
-    TEST_14 = auto()
-
-
-def pytest_configure():
-    pytest.PORT = TEST_PORT
diff --git a/tests/test_cxl_component_physical_port_manager.py b/tests/test_cxl_component_physical_port_manager.py
index f05f49ca..fbfb670f 100644
--- a/tests/test_cxl_component_physical_port_manager.py
+++ b/tests/test_cxl_component_physical_port_manager.py
@@ -16,11 +16,6 @@
     DownstreamPortDevice,
 )
 from opencis.cxl.component.switch_connection_manager import SwitchConnectionManager
-from opencis.util.number import get_rand_range_generator
-
-
-BASE_TEST_PORT = 9000
-generator = get_rand_range_generator(BASE_TEST_PORT, 100)
 
 
 @pytest.mark.asyncio
@@ -33,8 +28,7 @@ async def test_physical_port_manager_init(get_gold_std_reg_vals):
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    switch_connection_manager = SwitchConnectionManager(port_configs, port=port)
+    switch_connection_manager = SwitchConnectionManager(port_configs, port=0)
     physical_port_manager = PhysicalPortManager(
         switch_connection_manager=switch_connection_manager, port_configs=port_configs
     )
@@ -67,8 +61,7 @@ async def test_physical_port_manager_run_and_stop():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    switch_connection_manager = SwitchConnectionManager(port_configs, port=port)
+    switch_connection_manager = SwitchConnectionManager(port_configs, port=0)
     physical_port_manager = PhysicalPortManager(
         switch_connection_manager=switch_connection_manager, port_configs=port_configs
     )
@@ -90,8 +83,7 @@ async def test_physical_port_manager_stop_before_run():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    switch_connection_manager = SwitchConnectionManager(port_configs, port=port)
+    switch_connection_manager = SwitchConnectionManager(port_configs, port=0)
     physical_port_manager = PhysicalPortManager(
         switch_connection_manager=switch_connection_manager, port_configs=port_configs
     )
@@ -109,8 +101,7 @@ async def test_physical_port_manager_run_after_run():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    switch_connection_manager = SwitchConnectionManager(port_configs, port=port)
+    switch_connection_manager = SwitchConnectionManager(port_configs, port=0)
     physical_port_manager = PhysicalPortManager(
         switch_connection_manager=switch_connection_manager, port_configs=port_configs
     )
diff --git a/tests/test_cxl_component_switch_bind.py b/tests/test_cxl_component_switch_bind.py
index 0a933a90..b769683d 100644
--- a/tests/test_cxl_component_switch_bind.py
+++ b/tests/test_cxl_component_switch_bind.py
@@ -74,7 +74,7 @@ async def test_single_logical_device_bind_unbind():
         vppb_counts=vppb_counts,
         initial_bounds=initial_bounds,
         physical_ports=physical_ports,
-        irq_port=8500 + pytest.PORT.TEST_1,
+        irq_port=0,
         allocated_ld=allocated_ld,
     )
 
@@ -185,7 +185,7 @@ async def test_multi_logical_device_bind_unbind():
         vppb_counts=vppb_counts,
         initial_bounds=initial_bounds,
         physical_ports=physical_ports,
-        irq_port=8500 + pytest.PORT.TEST_1,
+        irq_port=0,
         allocated_ld=allocated_ld,
     )
 
diff --git a/tests/test_cxl_component_switch_connection_manager.py b/tests/test_cxl_component_switch_connection_manager.py
index c30676ae..86b6c832 100644
--- a/tests/test_cxl_component_switch_connection_manager.py
+++ b/tests/test_cxl_component_switch_connection_manager.py
@@ -39,11 +39,6 @@
     CXL_MEM_M2SBIRSP_OPCODE,
     CXL_MEM_S2MBISNP_OPCODE,
 )
-from opencis.util.number import get_rand_range_generator
-
-
-BASE_TEST_PORT = 9100
-generator = get_rand_range_generator(BASE_TEST_PORT, 100)
 
 
 def test_switch_connection_manager_check_ports():
@@ -53,8 +48,7 @@ def test_switch_connection_manager_check_ports():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     for port in range(len(port_configs)):
         connection = manager.get_cxl_connection(port)
         assert isinstance(connection, CxlConnection)
@@ -70,8 +64,7 @@ async def test_switch_connection_manager_run_and_stop():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
 
     async def wait_and_stop():
         await manager.wait_for_ready()
@@ -89,8 +82,7 @@ async def test_switch_connection_manager_run_and_run():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
 
     async def wait_and_run():
         await manager.wait_for_ready()
@@ -110,8 +102,7 @@ async def test_switch_connection_manager_stop_before_run():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
 
     with pytest.raises(Exception, match="Cannot stop when it is not running"):
         await manager.stop()
@@ -125,10 +116,9 @@ async def test_switch_connection_manager_handle_connection():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -139,6 +129,7 @@ async def wait_and_connect():
         await manager.wait_for_ready()
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
+        client.set_port(manager.get_port())
         await client.run()
 
     async def stop():
@@ -165,10 +156,9 @@ async def test_switch_connection_manager_handle_connection_oob():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=4, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=4, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -180,6 +170,7 @@ async def wait_and_connect():
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
         with pytest.raises(Exception, match="Connection rejected"):
+            client.set_port(manager.get_port())
             await client.run()
         await manager.stop()
 
@@ -200,10 +191,9 @@ async def test_switch_connection_manager_handle_connection_after_connection():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -214,6 +204,7 @@ async def wait_and_connect():
         await manager.wait_for_ready()
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
+        client.set_port(manager.get_port())
         await client.run()
 
     async def stop():
@@ -243,10 +234,9 @@ async def test_switch_connection_manager_handle_connection_errors():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -257,14 +247,17 @@ async def wait_and_connect():
         await manager.wait_for_ready()
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
+        client.set_port(manager.get_port())
         await client.run()
 
     async def wait_and_connect():
         await manager.wait_for_ready()
         with pytest.raises(Exception, match="Connection rejected"):
+            client.set_port(manager.get_port())
             client.inject_error(INJECTED_ERRORS.NON_SIDEBAND)
             await client.run()
         with pytest.raises(Exception, match="Connection rejected"):
+            client.set_port(manager.get_port())
             client.inject_error(INJECTED_ERRORS.NON_CONNNECTION_REQUEST)
             await client.run()
         await manager.stop()
@@ -281,10 +274,9 @@ async def test_switch_connection_manager_handle_cfg_packet():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -295,6 +287,7 @@ async def wait_and_connect():
         await manager.wait_for_ready()
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
+        client.set_port(manager.get_port())
         await client.run()
 
     async def send_packets_and_stop():
@@ -333,10 +326,9 @@ async def test_switch_connection_manager_handle_mmio_packet():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -347,6 +339,7 @@ async def wait_and_connect():
         await manager.wait_for_ready()
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
+        client.set_port(manager.get_port())
         await client.run()
 
     async def send_packets_and_stop():
@@ -384,10 +377,9 @@ async def test_switch_connection_manager_handle_cxl_mem_packet_m2s():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -398,6 +390,7 @@ async def wait_and_connect():
         await manager.wait_for_ready()
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
+        client.set_port(manager.get_port())
         await client.run()
 
     async def send_packets_and_stop():
@@ -437,10 +430,9 @@ async def test_switch_connection_manager_handle_cfg_completion():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -451,6 +443,7 @@ async def wait_and_connect():
         await manager.wait_for_ready()
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
+        client.set_port(manager.get_port())
         await client.run()
 
     async def send_packets_and_stop():
@@ -504,10 +497,9 @@ async def test_switch_connection_manager_handle_mmio_completion():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -518,6 +510,7 @@ async def wait_and_connect():
         await manager.wait_for_ready()
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
+        client.set_port(manager.get_port())
         await client.run()
 
     async def send_packets_and_stop():
@@ -572,10 +565,9 @@ async def test_switch_connection_manager_handle_cxl_mem_s2m():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    manager = SwitchConnectionManager(port_configs, port=port)
+    manager = SwitchConnectionManager(port_configs, port=0)
     client = SwitchConnectionClient(
-        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=port
+        port_index=0, component_type=CXL_COMPONENT_TYPE.R, retry=False, port=0
     )
 
     async def start():
@@ -586,6 +578,7 @@ async def wait_and_connect():
         await manager.wait_for_ready()
         logger.info("[PyTest] SwitchConnectionManager is ready")
         logger.info("[PyTest] Starting SwitchConnectionClient")
+        client.set_port(manager.get_port())
         await client.run()
 
     async def send_packets_and_stop():
diff --git a/tests/test_cxl_component_virtual_switch.py b/tests/test_cxl_component_virtual_switch.py
index 3c643718..29e1d99d 100644
--- a/tests/test_cxl_component_virtual_switch.py
+++ b/tests/test_cxl_component_virtual_switch.py
@@ -229,7 +229,7 @@ async def test_virtual_switch_manager_run_and_stop():
         vppb_counts=vppb_counts,
         initial_bounds=initial_bounds,
         physical_ports=physical_ports,
-        irq_port=8500 + pytest.PORT.TEST_1,
+        irq_port=0,
         allocated_ld=allocated_ld,
     )
 
diff --git a/tests/test_cxl_component_virtual_switch_manager.py b/tests/test_cxl_component_virtual_switch_manager.py
index d3339452..abb01622 100644
--- a/tests/test_cxl_component_virtual_switch_manager.py
+++ b/tests/test_cxl_component_virtual_switch_manager.py
@@ -22,11 +22,6 @@
     CxlVirtualSwitch,
 )
 from opencis.util.unaligned_bit_structure import UnalignedBitStructure
-from opencis.util.number import get_rand_range_generator
-
-
-BASE_TEST_PORT = 9200
-generator = get_rand_range_generator(BASE_TEST_PORT, 100)
 
 
 def test_virtual_switch_manager_init():
@@ -37,8 +32,7 @@ def test_virtual_switch_manager_init():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    switch_connection_manager = SwitchConnectionManager(port_configs, port=port)
+    switch_connection_manager = SwitchConnectionManager(port_configs, port=0)
     physical_port_manager = PhysicalPortManager(
         switch_connection_manager=switch_connection_manager, port_configs=port_configs
     )
@@ -50,7 +44,7 @@ def test_virtual_switch_manager_init():
             vppb_counts=vppb_counts,
             initial_bounds=initial_bounds,
             irq_host="127.0.0.1",
-            irq_port=next(generator),
+            irq_port=0,
         )
     ]
     allocated_ld = {}
@@ -78,8 +72,7 @@ async def test_virtual_switch_manager_run_and_stop():
         PortConfig(PORT_TYPE.DSP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    port = next(generator)
-    switch_connection_manager = SwitchConnectionManager(port_configs, port=port)
+    switch_connection_manager = SwitchConnectionManager(port_configs, port=0)
     physical_port_manager = PhysicalPortManager(
         switch_connection_manager=switch_connection_manager, port_configs=port_configs
     )
@@ -91,7 +84,7 @@ async def test_virtual_switch_manager_run_and_stop():
             vppb_counts=vppb_counts,
             initial_bounds=initial_bounds,
             irq_host="127.0.0.1",
-            irq_port=next(generator),
+            irq_port=0,
         )
     ]
     allocated_ld = {}
diff --git a/tests/test_cxl_host.py b/tests/test_cxl_host.py
index 84fa1a21..b723033b 100644
--- a/tests/test_cxl_host.py
+++ b/tests/test_cxl_host.py
@@ -34,10 +34,6 @@
 from opencis.apps.packet_trace_runner import PacketTraceRunner
 from opencis.util.memory import get_memory_bin_name
 from opencis.util.number_const import MB
-from opencis.util.number import get_rand_range_generator
-
-BASE_TEST_PORT = 9300
-generator = get_rand_range_generator(BASE_TEST_PORT, 100)
 
 
 class SimpleJsonClient:
@@ -148,13 +144,12 @@ async def send_util_and_check_host(host_client, util_client, cmd):
 
 @pytest.mark.asyncio
 async def test_cxl_host_manager_send_util_and_recv_host():
-    host_port = next(generator)
-    util_port = next(generator)
-
-    host_manager = HostManager(host_port=host_port, util_port=util_port)
+    host_manager = HostManager(host_port=0, util_port=0)
     asyncio.create_task(host_manager.run())
     await host_manager.wait_for_ready()
-    host_client, util_client = await init_clients(host_port, util_port)
+    host_client, util_client = await init_clients(
+        host_port=host_manager.get_host_port(), util_port=host_manager.get_util_port()
+    )
 
     cmd = request_json("UTIL:CXL_HOST_READ", params={"port": 0, "addr": 0x40})
     await send_util_and_check_host(host_client, util_client, cmd)
@@ -176,15 +171,12 @@ async def send_and_check_res(util_client: SimpleJsonClient, cmd: str, res_expect
 
 @pytest.mark.asyncio
 async def test_cxl_host_manager_handle_res():
-    host_port = next(generator)
-    util_port = next(generator)
-
-    host_manager = HostManager(host_port=host_port, util_port=util_port)
+    host_manager = HostManager(host_port=0, util_port=0)
     asyncio.create_task(host_manager.run())
     await host_manager.wait_for_ready()
     host = DummyHost()
-    asyncio.create_task(host.conn_open(port=host_port))
-    util_client = SimpleJsonClient(port=util_port)
+    asyncio.create_task(host.conn_open(port=host_manager.get_host_port()))
+    util_client = SimpleJsonClient(port=host_manager.get_util_port())
     await host.wait_connected()
 
     addr = 0x40
@@ -215,15 +207,12 @@ async def send_and_check_err(util_client: SimpleJsonClient, cmd: str, err_expect
 
 @pytest.mark.asyncio
 async def test_cxl_host_manager_handle_err():
-    host_port = next(generator)
-    util_port = next(generator)
-
-    host_manager = HostManager(host_port=host_port, util_port=util_port)
+    host_manager = HostManager(host_port=0, util_port=0)
     asyncio.create_task(host_manager.run())
     await host_manager.wait_for_ready()
     dummy_host = DummyHost()
-    asyncio.create_task(dummy_host.conn_open(port=host_port))
-    util_client = SimpleJsonClient(port=util_port)
+    asyncio.create_task(dummy_host.conn_open(port=host_manager.get_host_port()))
+    util_client = SimpleJsonClient(port=host_manager.get_util_port())
     await dummy_host.wait_connected()
     data = 0xA5A5
     valid_addr = 0x40
@@ -253,16 +242,13 @@ async def test_cxl_host_manager_handle_err():
 
 @pytest.mark.asyncio
 async def test_cxl_host_util_client():
-    host_port = next(generator)
-    util_port = next(generator)
-
-    host_manager = HostManager(host_port=host_port, util_port=util_port)
+    host_manager = HostManager(host_port=0, util_port=0)
     asyncio.create_task(host_manager.run())
     await host_manager.wait_for_ready()
     dummy_host = DummyHost()
-    asyncio.create_task(dummy_host.conn_open(port=host_port))
+    asyncio.create_task(dummy_host.conn_open(port=host_manager.get_host_port()))
     await dummy_host.wait_connected()
-    util_client = UtilConnClient(port=util_port)
+    util_client = UtilConnClient(port=host_manager.get_util_port())
 
     data = 0xA5A5
     valid_addr = 0x40
@@ -281,17 +267,11 @@ async def test_cxl_host_util_client():
 @pytest.mark.asyncio
 async def test_cxl_host_type3_ete():
     # pylint: disable=protected-access
-    host_port = next(generator)
-    util_port = next(generator)
-    switch_port = next(generator)
-    irq_port = next(generator)
-    mctp_port = next(generator)
-
     port_configs = [
         PortConfig(PORT_TYPE.USP),
         PortConfig(PORT_TYPE.DSP),
     ]
-    sw_conn_manager = SwitchConnectionManager(port_configs, port=switch_port)
+    sw_conn_manager = SwitchConnectionManager(port_configs, port=0)
     physical_port_manager = PhysicalPortManager(
         switch_connection_manager=sw_conn_manager, port_configs=port_configs
     )
@@ -302,7 +282,7 @@ async def test_cxl_host_type3_ete():
             vppb_counts=1,
             initial_bounds=[1],
             irq_host="127.0.0.1",
-            irq_port=next(generator),
+            irq_port=0,
         )
     ]
     allocated_ld = {}
@@ -312,43 +292,47 @@ async def test_cxl_host_type3_ete():
         physical_port_manager=physical_port_manager,
         allocated_ld=allocated_ld,
     )
-    sld = SingleLogicalDevice(
-        port_index=1,
-        memory_size=0x1000000,
-        memory_file=get_memory_bin_name(switch_port),
-        serial_number="DDDDDDDDDDDDDDDD",
-        port=switch_port,
-    )
 
-    # host_fm_conn_port port number conflict?
-    fabric_manager = CxlFabricManager(mctp_port=mctp_port, host_fm_conn_port=8700)
-    host_manager = HostManager(host_port=host_port, util_port=util_port)
+    fabric_manager = CxlFabricManager(mctp_port=0, host_fm_conn_port=0)
+    host_manager = HostManager(host_port=0, util_port=0)
 
     # 256B / No interleave
     ig = 0
     iw = 0
 
-    cxl_host_config = CxlHostConfig(
-        port_index=0,
-        sys_mem_size=(16 * MB),
-        sys_sw_app=lambda **kwargs: my_sys_sw_app(ig=ig, iw=iw, **kwargs),
-        user_app=lambda **kwargs: sample_app(keepalive=False, **kwargs),
-        switch_port=switch_port,
-        irq_port=irq_port,
-        host_conn_port=host_port,
-        enable_hm=False,
-    )
-    host = CxlHost(cxl_host_config)
     start_tasks = [
         await fabric_manager.run_wait_ready(),
         await sw_conn_manager.run_wait_ready(),
         await physical_port_manager.run_wait_ready(),
         await virtual_switch_manager.run_wait_ready(),
         await host_manager.run_wait_ready(),
-        await sld.run_wait_ready(),
-        await host.run_wait_ready(),
     ]
 
+    sld = SingleLogicalDevice(
+        port_index=1,
+        memory_size=0x1000000,
+        memory_file=get_memory_bin_name(),
+        serial_number="DDDDDDDDDDDDDDDD",
+        port=sw_conn_manager.get_port(),
+    )
+    start_tasks += [await sld.run_wait_ready()]
+
+    print(f"irq_port: {virtual_switch_manager.get_port(0)}")
+    cxl_host_config = CxlHostConfig(
+        port_index=0,
+        sys_mem_size=(16 * MB),
+        sys_sw_app=lambda **kwargs: my_sys_sw_app(
+            ig=ig, iw=iw, host_fm_conn_port=fabric_manager.get_host_fm_port(), **kwargs
+        ),
+        user_app=lambda **kwargs: sample_app(keepalive=False, **kwargs),
+        switch_port=sw_conn_manager.get_port(),
+        irq_port=virtual_switch_manager.get_port(0),
+        host_conn_port=host_manager.get_host_port(),
+        enable_hm=False,
+    )
+    host = CxlHost(cxl_host_config)
+    start_tasks += [await host.run_wait_ready()]
+
     data = 0xA5A5
     valid_addr = 0x40
     invalid_addr = 0x41
@@ -383,21 +367,21 @@ def get_trace_ports(file_name):
 @pytest.mark.asyncio
 async def test_cxl_qemu_host_type3():
     # pylint: disable=protected-access
-    switch_port = next(generator)
     start_tasks = []
     env = parse_cxl_environment("configs/1vcs_4sld.yaml")
-    env.switch_config.port = switch_port
+    env.switch_config.port = 0
     for vsconfig in env.switch_config.virtual_switch_configs:
-        vsconfig.irq_port = next(generator)
+        vsconfig.irq_port = 0
     switch = CxlSwitch(env.switch_config, env.logical_device_configs, start_mctp=False)
     start_tasks.append(await switch.run_wait_ready())
+    env.switch_config.port = switch.get_port()
 
     slds = []
     for i, config in enumerate(env.single_logical_device_configs):
         sld = SingleLogicalDevice(
             port_index=config.port_index,
             memory_size=config.memory_size,
-            memory_file=get_memory_bin_name(switch_port, i),
+            memory_file=get_memory_bin_name(i),
             serial_number=config.serial_number,
             host=env.switch_config.host,
             port=env.switch_config.port,
@@ -410,7 +394,7 @@ async def test_cxl_qemu_host_type3():
     trace_runner = PacketTraceRunner(
         pcap_file,
         "0.0.0.0",
-        switch_port,
+        env.switch_config.port,
         trace_switch_port,
         trace_device_port,
     )


Reviews:
user: login: kyeyoon-eeum
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T01:37:00Z
user: login: kyeyoon-eeum
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T03:59:15Z

Review comments:
diff_hunk: @@ -117,3 +117,6 @@ async def _stop(self):
         await self._connection_manager.stop()
         await self._socketio_server.stop()
         await self._api_client.stop()
+
+    def get_host_fm_port(self):
+        return self._host_fm_conn_server.get_port()
path: opencis/apps/fabric_manager.py
user: login: kyeyoon-eeum
body: Move to somewhere above `_run()`
created_at: 2025-04-05T00:58:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+"""
+Copyright (c) 2025, Eeum, Inc.
+
+This software is licensed under the terms of the Revised BSD License.
+See LICENSE for details.
+"""
+
+import asyncio
+from asyncio import CancelledError
+import sys
+from typing import Callable
+import traceback
+
+from opencis.util.component import RunnableComponent
+from opencis.util.logger import logger
+
+
+class ServerComponent(RunnableComponent):
+    def __init__(
+        self,
+        handle_client: Callable,
+        host: str = "0.0.0.0",
+        port: int = 0,
+        limit: int = 0,
+        stop_callback: Callable = None,
+        label=None,
path: opencis/util/server.py
user: login: kyeyoon-eeum
body: `label: str = None`?
created_at: 2025-04-05T01:03:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+"""
+Copyright (c) 2025, Eeum, Inc.
+
+This software is licensed under the terms of the Revised BSD License.
+See LICENSE for details.
+"""
+
+import asyncio
+from asyncio import CancelledError
+import sys
+from typing import Callable
+import traceback
+
+from opencis.util.component import RunnableComponent
+from opencis.util.logger import logger
+
+
+class ServerComponent(RunnableComponent):
+    def __init__(
+        self,
+        handle_client: Callable,
+        host: str = "0.0.0.0",
+        port: int = 0,
+        limit: int = 0,
+        stop_callback: Callable = None,
+        label=None,
+    ):
+        if label is None:
+            # Get caller function name
+            # pylint: disable=protected-access
+            label = sys._getframe(1).f_locals["self"].__class__.__name__
+        super().__init__(label)
+
+        if handle_client is None:
+            raise ValueError("handle_client must be provided")
+
+        self._host = host
+        self._port = port
+        self._limit = limit
+        self._handle_client = handle_client
+        self._stop_callback = stop_callback
+        self._descriptor = f"TCP server ({label})"
+        self._server_task = None
+        self._clients = set()
+
+    async def _run(self):
path: opencis/util/server.py
user: login: kyeyoon-eeum
body: Put it at the bottom above `_stop()`.
created_at: 2025-04-05T01:08:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+"""
+Copyright (c) 2025, Eeum, Inc.
+
+This software is licensed under the terms of the Revised BSD License.
+See LICENSE for details.
+"""
+
+import asyncio
+from asyncio import CancelledError
+import sys
+from typing import Callable
+import traceback
+
+from opencis.util.component import RunnableComponent
+from opencis.util.logger import logger
+
+
+class ServerComponent(RunnableComponent):
+    def __init__(
+        self,
+        handle_client: Callable,
+        host: str = "0.0.0.0",
+        port: int = 0,
+        limit: int = 0,
+        stop_callback: Callable = None,
+        label=None,
+    ):
+        if label is None:
+            # Get caller function name
+            # pylint: disable=protected-access
+            label = sys._getframe(1).f_locals["self"].__class__.__name__
+        super().__init__(label)
+
+        if handle_client is None:
+            raise ValueError("handle_client must be provided")
+
+        self._host = host
+        self._port = port
+        self._limit = limit
+        self._handle_client = handle_client
+        self._stop_callback = stop_callback
+        self._descriptor = f"TCP server ({label})"
+        self._server_task = None
+        self._clients = set()
+
+    async def _run(self):
+        try:
+            logger.info(self._create_message(f"Creating {self._descriptor}"))
+            server = await self._create_server()
+            self._server_task = asyncio.create_task(server.serve_forever())
+            while not server.is_serving():
+                await asyncio.sleep(0.1)
+            await self._change_status_to_running()
+            await self._server_task
+        except Exception as e:
+            logger.error(
+                self._create_message(
+                    f"{self._descriptor} error: {str(e)}, {traceback.format_exc()}"
+                )
+            )
+        except CancelledError:
+            logger.info(self._create_message(f"Stopped {self._descriptor}"))
+            if self._stop_callback is not None:
+                await self._stop_callback()
+
+    async def _stop(self):
path: opencis/util/server.py
user: login: kyeyoon-eeum
body: Put it at the bottom.
created_at: 2025-04-05T01:08:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,128 @@
+"""
+Copyright (c) 2025, Eeum, Inc.
+
+This software is licensed under the terms of the Revised BSD License.
+See LICENSE for details.
+"""
+
+import asyncio
+from asyncio import CancelledError
+import sys
+from typing import Callable
+import traceback
+
+from opencis.util.component import RunnableComponent
+from opencis.util.logger import logger
+
+
+class ServerComponent(RunnableComponent):
+    def __init__(
+        self,
+        handle_client: Callable,
+        host: str = "0.0.0.0",
+        port: int = 0,
+        limit: int = 0,
+        stop_callback: Callable = None,
+        label=None,
+    ):
+        if label is None:
+            # Get caller function name
+            # pylint: disable=protected-access
+            label = sys._getframe(1).f_locals["self"].__class__.__name__
+        super().__init__(label)
+
+        if handle_client is None:
+            raise ValueError("handle_client must be provided")
+
+        self._host = host
+        self._port = port
+        self._limit = limit
+        self._handle_client = handle_client
+        self._stop_callback = stop_callback
+        self._descriptor = f"TCP server ({label})"
+        self._server_task = None
+        self._clients = set()
+
+    async def _run(self):
+        try:
+            logger.info(self._create_message(f"Creating {self._descriptor}"))
+            server = await self._create_server()
+            self._server_task = asyncio.create_task(server.serve_forever())
+            while not server.is_serving():
+                await asyncio.sleep(0.1)
+            await self._change_status_to_running()
+            await self._server_task
+        except Exception as e:
+            logger.error(
+                self._create_message(
+                    f"{self._descriptor} error: {str(e)}, {traceback.format_exc()}"
+                )
+            )
+        except CancelledError:
+            logger.info(self._create_message(f"Stopped {self._descriptor}"))
+            if self._stop_callback is not None:
+                await self._stop_callback()
+
+    async def _stop(self):
+        logger.info(self._create_message(f"Cancelling {self._descriptor} task"))
+        self._server_task.cancel()
+        for client in self._clients.copy():
+            logger.info(
+                self._create_message(
+                    f'Closing client connection: {client.get_extra_info("peername")}'
+                )
+            )
+            client.close()
+        for client in self._clients.copy():
+            await client.wait_closed()
+            logger.info(
+                self._create_message(
+                    f'Closed client connection: {client.get_extra_info("peername")}'
+                )
+            )
+        self._clients.clear()
+        try:
+            await self._server_task
+        except CancelledError:
+            logger.info(self._create_message(f"Cancelled {self._descriptor}"))
+
+    async def _create_server(self):
+        async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
+            self._clients.add(writer)
+            logger.info(
+                self._create_message(
+                    f"Found a new socket connection: {writer.get_extra_info('peername')}"
+                )
+            )
+            await self._handle_client(reader, writer)
+            # Handled, now close the connection
+            self._clients.discard(writer)
+            description = writer.get_extra_info("peername")
+            writer.close()
+            try:
+                await writer.wait_closed()
+            except Exception as e:
+                logger.error(
+                    self._create_message(
+                        f"Error while closing {description}: {str(e)}, {traceback.format_exc()}"
+                    )
+                )
+            logger.info(self._create_message(f"Closed client connection: {description}"))
+
+        logger.info(self._create_message(f"Starting {self._descriptor} server"))
+        if self._limit == 0:
+            server = await asyncio.start_server(handle_client, self._host, self._port)
+        else:
+            server = await asyncio.start_server(
+                handle_client, self._host, self._port, limit=self._limit
+            )
path: opencis/util/server.py
user: login: kyeyoon-eeum
body: https://docs.python.org/3/library/asyncio-stream.html
```
async asyncio.start_server(client_connected_cb, host=None, port=None, *, limit=None, family=socket.AF_UNSPEC, flags=socket.AI_PASSIVE, sock=None, backlog=100, ssl=None, reuse_address=None, reuse_port=None, keep_alive=None, ssl_handshake_timeout=None, ssl_shutdown_timeout=None, start_serving=True)
```
"limit determines the buffer size limit used by the returned StreamReader instance. By default the limit is set to 64 KiB."

Not sure why we were setting `limit`. Please test and let's get rid of setting it.
created_at: 2025-04-05T01:17:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,117 +69,53 @@ def __init__(
         self._port = port
         self._connection_timeout_ms = connection_timeout_ms
         self._ports = [SwitchPort(port_config=port_config) for port_config in port_configs]
-        self._server_task = None
+        self._server_component = None
         self._event_handler = None
-        self._clients = set()
 
     async def _run(self):
+        self._server_component = ServerComponent(
+            handle_client=self._handle_client,
+            host=self._host,
+            port=self._port,
+        )
path: opencis/cxl/component/switch_connection_manager.py
user: login: kyeyoon-eeum
body: Define inside of `__init__()`
created_at: 2025-04-05T01:20:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,117 +69,53 @@ def __init__(
         self._port = port
         self._connection_timeout_ms = connection_timeout_ms
         self._ports = [SwitchPort(port_config=port_config) for port_config in port_configs]
-        self._server_task = None
+        self._server_component = None
         self._event_handler = None
-        self._clients = set()
 
     async def _run(self):
+        self._server_component = ServerComponent(
+            handle_client=self._handle_client,
+            host=self._host,
+            port=self._port,
+        )
+        server_task = create_task(self._server_component.run())
+        await self._server_component.wait_for_ready()
+        self._port = self._server_component.get_port()
+        await self._change_status_to_running()
+        await server_task
+
+    async def _stop(self):
path: opencis/cxl/component/switch_connection_manager.py
user: login: kyeyoon-eeum
body: Move to the bottom of the file.
created_at: 2025-04-05T01:25:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,117 +69,53 @@ def __init__(
         self._port = port
         self._connection_timeout_ms = connection_timeout_ms
         self._ports = [SwitchPort(port_config=port_config) for port_config in port_configs]
-        self._server_task = None
+        self._server_component = None
         self._event_handler = None
-        self._clients = set()
 
     async def _run(self):
path: opencis/cxl/component/switch_connection_manager.py
user: login: kyeyoon-eeum
body: Move towards the bottom just above `_stop()`.

created_at: 2025-04-05T01:25:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -190,3 +190,6 @@ async def _stop(self):
             stop_tasks.append(create_task(self._mctp_connection_client.stop()))
             stop_tasks.append(create_task(self._mctp_cci_executor.stop()))
         await gather(*stop_tasks)
+
+    def get_port(self):
+        return self._switch_connection_manager.get_port()
path: opencis/apps/cxl_switch.py
user: login: kyeyoon-eeum
body: Move to somewhere above `_run()`.
created_at: 2025-04-05T01:26:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -73,6 +75,9 @@ async def _stop(self):
         self._host_server.close()
         await self._host_server.wait_closed()
 
+    def get_port(self):
+        return self._port
+
path: opencis/cxl/component/host_manager.py
user: login: kyeyoon-eeum
body: Move to somewhere above `_run()`.
created_at: 2025-04-05T01:28:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -199,6 +206,9 @@ async def _stop(self):
         self._util_server.close()
         await self._util_server.wait_closed()
 
+    def get_port(self):
+        return self._port
+
path: opencis/cxl/component/host_manager.py
user: login: kyeyoon-eeum
body: Move to somewhere above `_run()`.
created_at: 2025-04-05T01:28:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,3 +278,9 @@ async def _stop(self):
             asyncio.create_task(self._util_conn_server.stop()),
         ]
         await asyncio.gather(*tasks)
+
+    def get_host_port(self):
+        return self._host_conn_server.get_port()
+
+    def get_util_port(self):
+        return self._util_conn_server.get_port()
path: opencis/cxl/component/host_manager.py
user: login: kyeyoon-eeum
body: Move to somewhere above `_run()`.
created_at: 2025-04-05T01:28:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,57 +41,44 @@ def __init__(
         self._connection_timeout_ms = connection_timeout_ms
         # TODO: Support receiving connections from CXL Devices
         self._switch_port = MctpPort()
-        self._server_task = None
+        self._server_component = None
 
     async def _run(self):
-        # pylint: disable=bare-except
-        try:
-            logger.info(self._create_message(f"Creating TCP server at port {self._port}"))
-            server = await self._create_server()
-            self._server_task = asyncio.create_task(server.serve_forever())
-            logger.info(self._create_message("Starting TCP server task"))
-            while not server.is_serving():
-                await asyncio.sleep(0.1)
-            await self._change_status_to_running()
-            await self._server_task
-        except Exception as e:
-            logger.debug(self._create_message(f"Exception: {str(e)}"))
-        except:
-            logger.info(self._create_message("Stopped TCP server"))
-            if self._switch_port.packet_processor is not None:
-                logger.info(self._create_message("Stopping PacketProcessor for Switch Port"))
-                await self._switch_port.packet_processor.stop()
-                logger.info(self._create_message("Stopped PacketProcessor for Switch Port"))
+        self._server_component = ServerComponent(
+            handle_client=self._handle_client,
+            host=self._host,
+            port=self._port,
+            stop_callback=self._stop_callback,
+        )
path: opencis/cxl/component/mctp/mctp_connection_manager.py
user: login: kyeyoon-eeum
body: Define inside of `__init__()`.
created_at: 2025-04-05T01:28:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -200,11 +195,17 @@ async def shutdown(self):
     async def _run(self):
         try:
             if self._server:
-                server = await self._create_server()
-                self._server_task = create_task(server.serve_forever())
-                while not server.is_serving():
-                    await asyncio.sleep(0.1)
-                self._tasks.append(self._server_task)
+                self._server_component = ServerComponent(
+                    handle_client=self._new_conn,
+                    host=self._addr,
+                    port=self._port,
+                    limit=2,
+                )
path: opencis/cxl/component/short_msg_conn.py
user: login: kyeyoon-eeum
body: Define inside of `__init()__`.
created_at: 2025-04-05T01:29:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -228,3 +229,6 @@ async def _stop(self):
         for handler in self._msg_handlers:
             handler.cancel()
         logger.debug(self._create_message("ShortMsg handlers cancelled"))
+
+    def get_port(self):
+        return self._port
path: opencis/cxl/component/short_msg_conn.py
user: login: kyeyoon-eeum
body: Same
created_at: 2025-04-05T01:29:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: kyeyoon-eeum
created_at: 2025-04-04T16:10:51Z
body: Merge conflict after PR merges.
author_association: CONTRIBUTOR
user: login: arter97
created_at: 2025-04-04T23:08:56Z
body: Rebased. Changes from #200 are also merged directly to ServerComponent.py.

Now, there's no need to have a separate `_close_connection()`.
author_association: CONTRIBUTOR