Skip to content

Refuse a start when the port is held on the other loopback - #453

Open
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:fix/port-guard-both-loopbacks
Open

Refuse a start when the port is held on the other loopback#453
kevin9327 wants to merge 1 commit into
CopilotKit:mainfrom
kevin9327:fix/port-guard-both-loopbacks

Conversation

@kevin9327

Copy link
Copy Markdown
Contributor

The desktop shell refuses to start when something already holds a port the deployment needs. That
refusal asked one loopback address; the readiness check immediately after it accepts an answer at
either. So a port held on ::1 alone was reported free, and the start went ahead into it — which is
precisely the case port_already_taken was written for.

The gap

stack.rs already records why one loopback is not enough, next to the readiness check:

/// Both loopbacks, in the order a person is most likely to type.
///
/// A process that binds one and not the other is normal rather than broken: Node resolves
/// `localhost` to `::1` and bun to `127.0.0.1`, so which one a service ends up on depends on what
/// started it. Asking both is how a check stays true either way.
const LOOPBACKS: [&str; 2] = ["127.0.0.1", "[::1]"];

answering_at honours that and tries both. The guard in front of it did not:

if std::net::TcpStream::connect_timeout(
    &std::net::SocketAddr::from(([127, 0, 0, 1], *port)),
    std::time::Duration::from_millis(300),
)

The two therefore disagreed about what "this port is in use" means, and the guard is the half whose
own documentation says what that costs:

Found the hard way: another deployment was listening on 3001, so the readiness check below was
satisfied by a server this shell had never started. Everything looked green and none of it was
ours. Checked before anything is spawned, because afterwards the two are indistinguishable from
outside.

A holder on ::1 slipped past the check and left the start to fail later, on something that names a
process rather than the port in the way.

The change

One helper, asking both loopback addresses. Nothing else moves: same message, same 300 ms, same
call sites.

Failing first

The test binds a listener on [::1]:0 and asks the guard about the port it got. Against main's
stack.rs:

$ RUSTUP_TOOLCHAIN=1.98.0-x86_64-pc-windows-msvc cargo test --manifest-path desktop/src-tauri/Cargo.toml --lib stack::
running 14 tests
...
test stack::tests::a_port_held_on_the_other_loopback_is_still_held ... FAILED

---- stack::tests::a_port_held_on_the_other_loopback_is_still_held stdout ----

thread 'stack::tests::a_port_held_on_the_other_loopback_is_still_held' (21776) panicked at src\stack.rs:666:60:
a held port is a problem

failures:
    stack::tests::a_port_held_on_the_other_loopback_is_still_held

test result: FAILED. 13 passed; 1 failed; 0 ignored; 0 measured; 72 filtered out; finished in 4.06s

With the change:

$ RUSTUP_TOOLCHAIN=1.98.0-x86_64-pc-windows-msvc cargo test --manifest-path desktop/src-tauri/Cargo.toml
test result: ok. 86 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.05s

85 of those 86 are the suite as it stands on main, all still passing.

Against over-correcting

The two cases that must stay accepted are already pinned by tests that ran before and after, both
green in each run above:

  • a_port_nobody_holds_is_not_reported_as_taken — port 1, which nothing is listening on, must not
    become a problem now that a second address is asked.
  • a_held_port_is_named_along_with_what_uses_it — the IPv4 case, unchanged, still named with its
    port and its purpose.

A refused connection comes back at once on both addresses, so the extra question does not add the
300 ms timeout to a start on a free port.

Verified with

RUSTUP_TOOLCHAIN=1.98.0-x86_64-pc-windows-msvc cargo test  --manifest-path desktop/src-tauri/Cargo.toml
RUSTUP_TOOLCHAIN=1.98.0-x86_64-pc-windows-msvc cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets
RUSTUP_TOOLCHAIN=1.98.0-x86_64-pc-windows-msvc cargo fmt --manifest-path desktop/src-tauri/Cargo.toml -- --check

clippy finished with no warnings on the crate; fmt --check is clean.

Note on the changelog

A deployment behaves differently afterwards — a start that used to go ahead now stops and names the
port — so there is a ## Unreleased entry. It shares that anchor with other open pull requests and
may need a one-line rebase.

The readiness check accepts an answer at 127.0.0.1 or at [::1], because a
process binds whichever loopback its runtime resolved `localhost` to. The
guard in front of it asked only 127.0.0.1, so a port held on ::1 alone was
reported free and the start went ahead into it -- the one outcome that guard
exists to prevent. Both addresses are asked now.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant