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 for what declare and start print, and Process management 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.