From 83195538ae7555f5b4e026d4a69d2a2abfac9453 Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Fri, 30 Oct 2026 00:17:07 -0300 Subject: [PATCH 1/3] Check the Linux packages' localhost command by unpacking them, and build them on pull requests that change packaging --- .github/workflows/packages.yml | 74 +++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 9 ++-- RELEASING.md | 2 +- scripts/check-linux-packages.sh | 59 ++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/packages.yml create mode 100755 scripts/check-linux-packages.sh diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml new file mode 100644 index 0000000..f2cb8eb --- /dev/null +++ b/.github/workflows/packages.yml @@ -0,0 +1,74 @@ +name: Linux packages + +# Builds the .deb, .rpm and AppImage as the release does, unsigned, and +# checks the localhost command in each (scripts/check-linux-packages.sh), so +# a pull request that changes how they are built finds out before a tag does. +on: + pull_request: + paths: + - .github/workflows/packages.yml + - .github/workflows/release.yml + - scripts/check-linux-packages.sh + - apps/desktop/src-tauri/tauri*.json + - apps/desktop/src-tauri/Cargo.toml + - apps/desktop/src-tauri/src/agents.rs + - core/localhost-cli/** + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: packages-${{ github.ref }} + cancel-in-progress: true + +jobs: + linux: + name: Build and check the Linux packages + runs-on: ubuntu-22.04 + timeout-minutes: 45 + defaults: + run: + working-directory: apps/desktop + env: + # Any version: the check compares the command's to it. + VERSION: 0.0.0 + steps: + - uses: actions/checkout@v4 + - name: Install the webview and tray libraries, and the package tools + run: > + sudo apt-get update && sudo apt-get install -y + libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev rpm cpio + - uses: dtolnay/rust-toolchain@1.90 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: | + core + apps/desktop/src-tauri + - uses: pnpm/action-setup@v4 + with: + package_json_file: apps/desktop/package.json + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + cache-dependency-path: apps/desktop/pnpm-lock.yaml + - run: pnpm install --frozen-lockfile + + # As in release.yml. + - name: Build the localhost command + env: + LOCALHOST_VERSION: ${{ env.VERSION }} + run: | + cargo build --manifest-path ../../core/Cargo.toml -p localhost-cli --bin localhost --release + target=$(rustc -vV | sed -n 's/^host: //p') + mkdir -p src-tauri/binaries + cp ../../core/target/release/localhost "src-tauri/binaries/localhost-$target" + + - name: Build the .deb, .rpm and AppImage (unsigned) + run: | + config=$(jq -nc --arg version "$VERSION" '{version: $version, bundle: {externalBin: ["binaries/localhost"]}}') + pnpm tauri build --bundles deb,rpm,appimage --config "$config" + + - name: Check the localhost command in every package + run: ../../scripts/check-linux-packages.sh src-tauri/target/release/bundle "$VERSION" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e916589..77c6e30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -232,7 +232,7 @@ jobs: - name: Install the webview and tray libraries run: > sudo apt-get update && sudo apt-get install -y - libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev rpm + libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev rpm cpio - uses: dtolnay/rust-toolchain@1.90 - uses: pnpm/action-setup@v4 with: @@ -274,9 +274,10 @@ jobs: {version: $version, bundle: {createUpdaterArtifacts: true, externalBin: ["binaries/localhost"]}, plugins: {updater: {pubkey: $key, endpoints: [$feed]}}}') pnpm tauri build --bundles deb,rpm,appimage --config "$config" - # The command is in every package. - dpkg-deb --contents src-tauri/target/release/bundle/deb/*.deb | grep -E ' \./usr/bin/localhost$' - rpm -qlp src-tauri/target/release/bundle/rpm/*.rpm | grep -x /usr/bin/localhost + + # Each package unpacked: usr/bin/localhost is there, runs, and says this version. + - name: Check the localhost command in every package + run: ../../scripts/check-linux-packages.sh src-tauri/target/release/bundle "${{ needs.version.outputs.version }}" - name: Collect and checksum env: diff --git a/RELEASING.md b/RELEASING.md index 1461e7c..a7cb773 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,6 +1,6 @@ # Releasing localhost -Releases are on GitHub Releases: a DMG for macOS; for Linux, a `.deb`, an `.rpm` and an AppImage; and an installer for Windows. Pushing a `v*` tag runs the [Release workflow](.github/workflows/release.yml). It builds and signs the macOS app into `localhost-.dmg`, and builds the Linux app (`apps/desktop/`) on Ubuntu 22.04 into `localhost-app__amd64.deb`, `localhost-app--1.x86_64.rpm` and `localhost-app__amd64.AppImage`, unsigned. For Windows it builds `localhostd` static for Linux (`x86_64-unknown-linux-musl`), then the same app on Windows into the per-user NSIS installer `localhost__x64-setup.exe`, with `localhostd` installed beside `localhost-app.exe` for the WSL distros. The installer is signed when a certificate exists (below). Every app bundles the `localhost` command of the same commit, which coding agents run as their MCP server: the macOS app in `Contents/Helpers/localhost`, signed like the app; the Linux packages as `/usr/bin/localhost`; the Windows installer as `localhost.exe` beside `localhost-app.exe`. Each job checks that it reports the release's version. Then it drafts one GitHub Release with all of them, a SHA-256 for each, and notes generated from the merged PRs. Nothing is public until a maintainer publishes the draft. +Releases are on GitHub Releases: a DMG for macOS; for Linux, a `.deb`, an `.rpm` and an AppImage; and an installer for Windows. Pushing a `v*` tag runs the [Release workflow](.github/workflows/release.yml). It builds and signs the macOS app into `localhost-.dmg`, and builds the Linux app (`apps/desktop/`) on Ubuntu 22.04 into `localhost-app__amd64.deb`, `localhost-app--1.x86_64.rpm` and `localhost-app__amd64.AppImage`, unsigned. For Windows it builds `localhostd` static for Linux (`x86_64-unknown-linux-musl`), then the same app on Windows into the per-user NSIS installer `localhost__x64-setup.exe`, with `localhostd` installed beside `localhost-app.exe` for the WSL distros. The installer is signed when a certificate exists (below). Every app bundles the `localhost` command of the same commit, which coding agents run as their MCP server: the macOS app in `Contents/Helpers/localhost`, signed like the app; the Linux packages as `/usr/bin/localhost`; the Windows installer as `localhost.exe` beside `localhost-app.exe`. Each job checks that it reports the release's version; for Linux, `scripts/check-linux-packages.sh` unpacks the `.deb`, the `.rpm` and the AppImage and runs the command in each. The Linux packages workflow does the same, unsigned, on pull requests that change how the packages are built. Then it drafts one GitHub Release with all of them, a SHA-256 for each, and notes generated from the merged PRs. Nothing is public until a maintainer publishes the draft. The release also carries the update feeds that installed apps check: `latest.json` for the Linux and Windows app and `appcast.xml` for the macOS app, both naming this release's packages, each signed with the updater keys (see [Updates](#updates)). Apps read them from `releases/latest/download/`, so an update reaches users only once its release is published, and a draft or pre-release never. diff --git a/scripts/check-linux-packages.sh b/scripts/check-linux-packages.sh new file mode 100755 index 0000000..1a99746 --- /dev/null +++ b/scripts/check-linux-packages.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Checks that every Linux package carries the `localhost` command coding +# agents run: usr/bin/localhost (on the PATH as /usr/bin/localhost once the +# .deb or .rpm is installed; inside the AppImage, which the app copies out +# for agents), executable, reporting the expected version. Each package is +# unpacked, so this doesn't depend on how a tool lists its paths. Needs +# dpkg-deb, rpm2cpio and cpio. +# +# scripts/check-linux-packages.sh +# +# is Tauri's output (src-tauri/target/release/bundle). +set -euo pipefail + +bundle=$(cd "${1:?usage: scripts/check-linux-packages.sh }" && pwd) +version=${2:?usage: scripts/check-linux-packages.sh } +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +check() { + local package=$1 root=$2 command=$2/usr/bin/localhost said + if [[ ! -x $command ]]; then + echo "error: $package has no executable usr/bin/localhost. Its usr/bin holds:" >&2 + ls -l "$root/usr/bin" >&2 2>&1 || true + return 1 + fi + said=$("$command" --version) + if [[ $said != "localhost $version" ]]; then + echo "error: $package's usr/bin/localhost says '$said', not 'localhost $version'" >&2 + return 1 + fi + echo "$package: usr/bin/localhost, $said" +} + +one() { + # shellcheck disable=SC2206 # $1 is a pattern, expanded here on purpose + local files=("$bundle"/$1) + if [[ ! -f ${files[0]} ]]; then + echo "error: no $1 in $bundle" >&2 + return 1 + fi + echo "${files[0]}" +} + +failed=0 +mkdir "$work/deb" "$work/rpm" "$work/appimage" +if deb=$(one 'deb/*.deb'); then + dpkg-deb -x "$deb" "$work/deb" + check .deb "$work/deb" || failed=1 +else failed=1; fi +if rpm=$(one 'rpm/*.rpm'); then + (cd "$work/rpm" && rpm2cpio "$rpm" | cpio -idm --quiet) + check .rpm "$work/rpm" || failed=1 +else failed=1; fi +if appimage=$(one 'appimage/*.AppImage'); then + # Unpacking needs no FUSE. + (cd "$work/appimage" && "$appimage" --appimage-extract > /dev/null) + check AppImage "$work/appimage/squashfs-root" || failed=1 +else failed=1; fi +exit $failed From 89e1fc28dca0ce097912cc4dd0e4a3c116b4c946 Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Fri, 30 Oct 2026 00:29:44 -0300 Subject: [PATCH 2/3] Build the Linux packages only by hand, and say which step failed while checking them --- .github/workflows/packages.yml | 17 ++----- .github/workflows/release.yml | 2 +- RELEASING.md | 17 +++++-- scripts/check-linux-packages.sh | 88 +++++++++++++++++++++------------ 4 files changed, 75 insertions(+), 49 deletions(-) diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index f2cb8eb..32f2ba0 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -1,18 +1,11 @@ name: Linux packages # Builds the .deb, .rpm and AppImage as the release does, unsigned, and -# checks the localhost command in each (scripts/check-linux-packages.sh), so -# a pull request that changes how they are built finds out before a tag does. +# checks the localhost command in each (scripts/check-linux-packages.sh). +# Run by hand on the release commit before tagging (RELEASING.md), so a +# packaging problem shows up before a tag does: +# gh workflow run packages.yml -R marcellosso/localhost --ref on: - pull_request: - paths: - - .github/workflows/packages.yml - - .github/workflows/release.yml - - scripts/check-linux-packages.sh - - apps/desktop/src-tauri/tauri*.json - - apps/desktop/src-tauri/Cargo.toml - - apps/desktop/src-tauri/src/agents.rs - - core/localhost-cli/** workflow_dispatch: permissions: @@ -38,7 +31,7 @@ jobs: - name: Install the webview and tray libraries, and the package tools run: > sudo apt-get update && sudo apt-get install -y - libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev rpm cpio + libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev libarchive-tools - uses: dtolnay/rust-toolchain@1.90 - uses: Swatinem/rust-cache@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 77c6e30..b0dbdbf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -232,7 +232,7 @@ jobs: - name: Install the webview and tray libraries run: > sudo apt-get update && sudo apt-get install -y - libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev rpm cpio + libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev libarchive-tools - uses: dtolnay/rust-toolchain@1.90 - uses: pnpm/action-setup@v4 with: diff --git a/RELEASING.md b/RELEASING.md index a7cb773..001c52e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,6 +1,6 @@ # Releasing localhost -Releases are on GitHub Releases: a DMG for macOS; for Linux, a `.deb`, an `.rpm` and an AppImage; and an installer for Windows. Pushing a `v*` tag runs the [Release workflow](.github/workflows/release.yml). It builds and signs the macOS app into `localhost-.dmg`, and builds the Linux app (`apps/desktop/`) on Ubuntu 22.04 into `localhost-app__amd64.deb`, `localhost-app--1.x86_64.rpm` and `localhost-app__amd64.AppImage`, unsigned. For Windows it builds `localhostd` static for Linux (`x86_64-unknown-linux-musl`), then the same app on Windows into the per-user NSIS installer `localhost__x64-setup.exe`, with `localhostd` installed beside `localhost-app.exe` for the WSL distros. The installer is signed when a certificate exists (below). Every app bundles the `localhost` command of the same commit, which coding agents run as their MCP server: the macOS app in `Contents/Helpers/localhost`, signed like the app; the Linux packages as `/usr/bin/localhost`; the Windows installer as `localhost.exe` beside `localhost-app.exe`. Each job checks that it reports the release's version; for Linux, `scripts/check-linux-packages.sh` unpacks the `.deb`, the `.rpm` and the AppImage and runs the command in each. The Linux packages workflow does the same, unsigned, on pull requests that change how the packages are built. Then it drafts one GitHub Release with all of them, a SHA-256 for each, and notes generated from the merged PRs. Nothing is public until a maintainer publishes the draft. +Releases are on GitHub Releases: a DMG for macOS; for Linux, a `.deb`, an `.rpm` and an AppImage; and an installer for Windows. Pushing a `v*` tag runs the [Release workflow](.github/workflows/release.yml). It builds and signs the macOS app into `localhost-.dmg`, and builds the Linux app (`apps/desktop/`) on Ubuntu 22.04 into `localhost-app__amd64.deb`, `localhost-app--1.x86_64.rpm` and `localhost-app__amd64.AppImage`, unsigned. For Windows it builds `localhostd` static for Linux (`x86_64-unknown-linux-musl`), then the same app on Windows into the per-user NSIS installer `localhost__x64-setup.exe`, with `localhostd` installed beside `localhost-app.exe` for the WSL distros. The installer is signed when a certificate exists (below). Every app bundles the `localhost` command of the same commit, which coding agents run as their MCP server: the macOS app in `Contents/Helpers/localhost`, signed like the app; the Linux packages as `/usr/bin/localhost`; the Windows installer as `localhost.exe` beside `localhost-app.exe`. Each job checks that it reports the release's version; for Linux, `scripts/check-linux-packages.sh` unpacks the `.deb`, the `.rpm` and the AppImage and runs the command in each. The Linux packages workflow does the same, unsigned, when run by hand. Then it drafts one GitHub Release with all of them, a SHA-256 for each, and notes generated from the merged PRs. Nothing is public until a maintainer publishes the draft. The release also carries the update feeds that installed apps check: `latest.json` for the Linux and Windows app and `appcast.xml` for the macOS app, both naming this release's packages, each signed with the updater keys (see [Updates](#updates)). Apps read them from `releases/latest/download/`, so an update reaches users only once its release is published, and a draft or pre-release never. @@ -13,7 +13,14 @@ Tags are `vMAJOR.MINOR.PATCH`, nothing else; the workflow rejects any other `v*` ## Cutting a release 1. Make sure `main` has everything and CI is green. -2. Tag the commit and push the tag: +2. Run **Linux packages** by hand on the release commit, and wait for it to pass. Pull-request CI doesn't build the packages; this builds the `.deb`, the `.rpm` and the AppImage as the release does, unsigned, and checks the bundled `localhost` command in each (`scripts/check-linux-packages.sh`): + + ```sh + gh workflow run packages.yml -R marcellosso/localhost --ref + gh run watch -R marcellosso/localhost "$(gh run list -R marcellosso/localhost --workflow packages.yml --limit 1 --json databaseId -q '.[0].databaseId')" + ``` + +3. Tag the commit and push the tag: ```sh git switch main && git pull @@ -21,9 +28,9 @@ Tags are `vMAJOR.MINOR.PATCH`, nothing else; the workflow rejects any other `v*` git push origin v0.3.0 ``` -3. Wait for the **Release** workflow (Actions tab). It ends with a draft release named "localhost 0.3.0". -4. Open the draft. Edit the notes if needed (the first line says how the build is signed and what the first launch needs). Download the DMG and check that it opens, installs and launches, following the README's first-launch steps. Install the `.deb` on Ubuntu and the `.rpm` on Fedora (or run the AppImage) and check that the app launches and its tray icon appears. On Windows 11 with Ubuntu in WSL 2, run the installer and check that a Project in the distro starts, opens in the browser, and stops when localhost quits. -5. Click **Publish release**. Installed apps see the update at their next check (at launch, or within six hours). Check that one does: open the previous version, use **Check for Updates…**, and install the update with a Service running, which should be listed and stopped first. +4. Wait for the **Release** workflow (Actions tab). It ends with a draft release named "localhost 0.3.0". +5. Open the draft. Edit the notes if needed (the first line says how the build is signed and what the first launch needs). Download the DMG and check that it opens, installs and launches, following the README's first-launch steps. Install the `.deb` on Ubuntu and the `.rpm` on Fedora (or run the AppImage) and check that the app launches and its tray icon appears. On Windows 11 with Ubuntu in WSL 2, run the installer and check that a Project in the distro starts, opens in the browser, and stops when localhost quits. +6. Click **Publish release**. Installed apps see the update at their next check (at launch, or within six hours). Check that one does: open the previous version, use **Check for Updates…**, and install the update with a Service running, which should be listed and stopped first. A fix is a normal change: branch → PR → merge → a patch tag (`v0.3.1`). diff --git a/scripts/check-linux-packages.sh b/scripts/check-linux-packages.sh index 1a99746..d9b05cb 100755 --- a/scripts/check-linux-packages.sh +++ b/scripts/check-linux-packages.sh @@ -3,27 +3,66 @@ # agents run: usr/bin/localhost (on the PATH as /usr/bin/localhost once the # .deb or .rpm is installed; inside the AppImage, which the app copies out # for agents), executable, reporting the expected version. Each package is -# unpacked, so this doesn't depend on how a tool lists its paths. Needs -# dpkg-deb, rpm2cpio and cpio. +# unpacked, so this doesn't depend on how a tool lists its paths. Every +# package is checked, and every failure says what failed. Needs dpkg-deb and +# bsdtar (libarchive-tools), which reads an .rpm whatever its compression. # # scripts/check-linux-packages.sh # # is Tauri's output (src-tauri/target/release/bundle). -set -euo pipefail +set -uo pipefail -bundle=$(cd "${1:?usage: scripts/check-linux-packages.sh }" && pwd) -version=${2:?usage: scripts/check-linux-packages.sh } +usage="usage: scripts/check-linux-packages.sh " +bundle=$(cd "${1:?$usage}" && pwd) || exit 1 +version=${2:?$usage} work=$(mktemp -d) trap 'rm -rf "$work"' EXIT +# Anything else failing still says so. +trap 'echo "error: line $LINENO: \`$BASH_COMMAND\` exited $?" >&2' ERR + +for tool in dpkg-deb bsdtar; do + command -v "$tool" > /dev/null || { echo "error: $tool isn't installed" >&2; exit 1; } +done + +# The one file matching a pattern under the bundle folder. +one() { + local files + # shellcheck disable=SC2206 # $1 is a pattern, expanded here on purpose + files=("$bundle"/$1) + if [[ ! -f ${files[0]} ]]; then + echo "error: no $1 in $bundle" >&2 + return 1 + fi + echo "${files[0]}" +} + +# Unpacks a package into a folder, saying why if it can't. +unpack() { + local kind=$1 file=$2 into=$3 output + mkdir -p "$into" + case $kind in + .deb) output=$(dpkg-deb -x "$file" "$into" 2>&1) ;; + .rpm) output=$(bsdtar -xf "$file" -C "$into" 2>&1) ;; + AppImage) output=$(cd "$into" && "$file" --appimage-extract 2>&1 > /dev/null) ;; + esac + local status=$? + if (( status != 0 )); then + echo "error: couldn't unpack the $kind ($(basename "$file")), exit $status: $output" >&2 + return 1 + fi +} check() { local package=$1 root=$2 command=$2/usr/bin/localhost said if [[ ! -x $command ]]; then echo "error: $package has no executable usr/bin/localhost. Its usr/bin holds:" >&2 - ls -l "$root/usr/bin" >&2 2>&1 || true + ls -l "$root/usr/bin" >&2 2>&1 || echo "(no usr/bin; it holds: $(ls "$root"))" >&2 + return 1 + fi + if ! said=$("$command" --version 2>&1); then + echo "error: $package's usr/bin/localhost --version failed: $said" >&2 return 1 fi - said=$("$command" --version) if [[ $said != "localhost $version" ]]; then echo "error: $package's usr/bin/localhost says '$said', not 'localhost $version'" >&2 return 1 @@ -31,29 +70,16 @@ check() { echo "$package: usr/bin/localhost, $said" } -one() { - # shellcheck disable=SC2206 # $1 is a pattern, expanded here on purpose - local files=("$bundle"/$1) - if [[ ! -f ${files[0]} ]]; then - echo "error: no $1 in $bundle" >&2 - return 1 - fi - echo "${files[0]}" -} - failed=0 -mkdir "$work/deb" "$work/rpm" "$work/appimage" -if deb=$(one 'deb/*.deb'); then - dpkg-deb -x "$deb" "$work/deb" - check .deb "$work/deb" || failed=1 -else failed=1; fi -if rpm=$(one 'rpm/*.rpm'); then - (cd "$work/rpm" && rpm2cpio "$rpm" | cpio -idm --quiet) - check .rpm "$work/rpm" || failed=1 -else failed=1; fi -if appimage=$(one 'appimage/*.AppImage'); then - # Unpacking needs no FUSE. - (cd "$work/appimage" && "$appimage" --appimage-extract > /dev/null) - check AppImage "$work/appimage/squashfs-root" || failed=1 -else failed=1; fi +for package in ".deb:deb/*.deb" ".rpm:rpm/*.rpm" "AppImage:appimage/*.AppImage"; do + kind=${package%%:*} pattern=${package#*:} + root=$work/${kind#.} + if file=$(one "$pattern") && unpack "$kind" "$file" "$root"; then + [[ $kind == AppImage ]] && root=$root/squashfs-root + check "$kind" "$root" || failed=1 + else + failed=1 + fi +done +(( failed == 0 )) && echo "Every package carries localhost $version." || echo "error: see above" >&2 exit $failed From 3fb5c979eea6364deccf0074dfbcce66e8295d35 Mon Sep 17 00:00:00 2001 From: Marcel Losso Date: Fri, 30 Oct 2026 00:53:14 -0300 Subject: [PATCH 3/3] Hand tests only ports with 100 free above them, so a moved Port never overflows --- core/localhost-core/tests/common/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/localhost-core/tests/common/mod.rs b/core/localhost-core/tests/common/mod.rs index eff1913..b007760 100644 --- a/core/localhost-core/tests/common/mod.rs +++ b/core/localhost-core/tests/common/mod.rs @@ -253,14 +253,15 @@ pub async fn wait_until(mut condition: impl FnMut() -> bool) { /// A port nothing listens on right now (bound once by the kernel's choice, /// then released), at least 100 away from any port another test of this /// binary was given: tests run in parallel, the kernel picks nearby ports, -/// and a Service whose Port is taken moves up to 100 ports above it. +/// and a Service whose Port is taken moves up to 100 ports above it, so +/// those 100 ports must exist too (Windows hands out ports up to 65535). pub fn free_port() -> u16 { static HANDED_OUT: Mutex>> = Mutex::new(None); loop { let port = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port(); let mut handed_out = HANDED_OUT.lock().unwrap(); let ports = handed_out.get_or_insert_with(HashSet::new); - if ports.iter().all(|other| other.abs_diff(port) > 100) { + if port <= u16::MAX - 100 && ports.iter().all(|other| other.abs_diff(port) > 100) { ports.insert(port); return port; }