# Ports Every device server on a bench binds one TCP port, and the bench is the only thing that sees all of them at once. Left alone, every device would take plesty-lib's default 5555 and the second server to start would fail — so allocating ports, remembering them, and moving them when they are taken is the bench's job. The rules live in `plesty/server/model/ports.py`. ## The reserved numbers | | | |---|---| | `5550` | `AGENT_PORT` — the bench agent's own. Never handed to a device. | | `5551` | `DEVICE_PORT_BASE` — the first port a device can be given. | | `5555` | `DEFAULT_TCP_PORT` — plesty-lib's default, what a server binds when nothing tells it otherwise. | ## Where a port comes from `port_from(args, env)` resolves what a server *would* bind, in this order: 1. `--tcp-port` or `-tp` in the declaration's `args` (as `--tcp-port 5560` or `--tcp-port=5560`) 2. any `*_TCP_PORT` key in the declaration's `env`, taking the keys in sorted order 3. `DEFAULT_TCP_PORT` — 5555 A declaration that reaches step 3 is one the bench must allocate for, because 5555 is what *every* such declaration would take. ## Named versus allocated This is the distinction the rest of the behaviour turns on. A **named** port is one the declaration itself carries, in `args` or `env`. `declares_port(args, env)` is true for it. The bench does not choose it and cannot change it: that number is passed to the device process, so a server started anywhere else would contradict its own arguments. An **allocated** port is one the bench chose, because the declaration named none. `declare` calls `allocate_port(taken)` — the lowest free number from 5551 up, skipping the agent's port, anything another device on this bench holds, and anything the host is already listening on — then writes it into `fleet.yaml`. From that moment `DeviceSpec.port_is_explicit` is true: the port is written down, so the port a client connected to yesterday is the port it finds today. ## When the port is taken Two benches on one machine allocate from the same range, and a server left running from an earlier start holds whatever it holds. A busy port serves no client, whoever is at fault, so the two cases diverge: | | Named port | Allocated port | |---|---|---| | On `start`, port busy | Refused with `PortBusy` | Re-allocated to a free port, recorded, started there | | Why | The number belongs to the device's own arguments | The number was only ever the bench's choice | The move is written back to `fleet.yaml` and reported, so nothing has to be guessed afterwards. See [Command line](cli.md) for what `declare` and `start` print, and [Process management](process_management.md) for where `PortBusy` is raised. ## Asking the host, not the records `fleet.yaml` and the process records say what this bench *believes* it is using. A server that outlived its record believes nothing, and neither does any other program on the machine — so allocation asks the socket, not the bookkeeping. Two functions ask, and they are not interchangeable: - **`port_is_free(port)`** binds it. Used when choosing a port to hand out. - **`port_listening(port)`** connects to it. Used when deciding whether something is already serving there. The distinction is a platform one. A device server binds `tcp://*`, and on some hosts a specific-address bind still succeeds beside a wildcard listener — connecting is the one test that answers the question everywhere. The bind probe also sets a different socket option per platform, because the platforms disagree about what `SO_REUSEADDR` means: on POSIX it says "a port left in `TIME_WAIT` is still free", which is the question being asked; on Windows it says "bind even if another socket is listening here", which is not. Windows uses `SO_EXCLUSIVEADDRUSE` instead.