diff --git a/README.md b/README.md index 0644b7d..8c60325 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,34 @@ npx harper-integration-test-setup-loopback This script requires `sudo` and respects the `HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT` environment variable (defaults to 32). +On macOS the aliases are configured with a host (`/32`) netmask. This matters: with the default class-A (`/8`) netmask every `127.0.0.x` alias claims the whole `127.0.0.0/8` network, and at large pool counts the resulting set of overlapping-subnet interface addresses drives `mDNSResponder`'s address-conflict defense into a CPU-pinning storm of mDNS announcements on port 5353. If you previously ran an older version of this script, re-run it to convert the existing aliases to `/32`. + +### Persisting across reboots (macOS) + +`ifconfig` aliases live only in the running kernel — macOS does not persist manually added `lo0` aliases, so they vanish on every reboot and the pool has to be reconfigured. For a dev machine that reboots regularly, install a `launchd` daemon that runs the setup script at boot. `scripts/io.harperdb.loopback-setup.plist` is provided for this; the script is root-aware, so it needs no `sudo` when launchd runs it as root. + +```sh +# 1. Copy the setup script to a stable, root-owned path (the plist points here) +sudo mkdir -p /usr/local/sbin +sudo install -m 755 -o root -g wheel \ + "$(npm root)/@harperfast/integration-testing/scripts/setup-loopback.sh" \ + /usr/local/sbin/harper-loopback-setup.sh + +# 2. Install the daemon (must be root:wheel and mode 644 or launchd rejects it) +sudo install -m 644 -o root -g wheel \ + "$(npm root)/@harperfast/integration-testing/scripts/io.harperdb.loopback-setup.plist" \ + /Library/LaunchDaemons/io.harperdb.loopback-setup.plist + +# 3. Load and run it now (also runs at every boot via RunAtLoad) +sudo launchctl bootstrap system /Library/LaunchDaemons/io.harperdb.loopback-setup.plist + +# 4. Verify +cat /var/log/harper-loopback-setup.log # ✓ Configured ... line +ifconfig lo0 | grep '127.0.0' | tail -3 # aliases present +``` + +Edit the `HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT` value in the installed plist to change the pool size (it defaults to 32, matching the script). To remove the daemon: `sudo launchctl bootout system/io.harperdb.loopback-setup` and delete the plist. If you edit the installed plist, `bootout` then `bootstrap` again to reload it. + ## API The lifecycle and utility APIs below are framework-agnostic. They manage Harper child processes and a cross-process loopback address pool. Use them in the setup/teardown hooks of whichever test framework you prefer. diff --git a/scripts/io.harperdb.loopback-setup.plist b/scripts/io.harperdb.loopback-setup.plist new file mode 100644 index 0000000..5fe14d3 --- /dev/null +++ b/scripts/io.harperdb.loopback-setup.plist @@ -0,0 +1,30 @@ + + + + + Label + io.harperdb.loopback-setup + + ProgramArguments + + /bin/bash + /usr/local/sbin/harper-loopback-setup.sh + + + + RunAtLoad + + + + EnvironmentVariables + + HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT + 32 + + + StandardOutPath + /var/log/harper-loopback-setup.log + StandardErrorPath + /var/log/harper-loopback-setup.log + + diff --git a/scripts/setup-loopback.sh b/scripts/setup-loopback.sh index 1100b99..7870e89 100755 --- a/scripts/setup-loopback.sh +++ b/scripts/setup-loopback.sh @@ -1,7 +1,14 @@ #!/bin/bash -# Prompt for password upfront -sudo -v +# Run ifconfig via sudo when invoked interactively, or directly when already root +# (e.g. from the launchd daemon at boot, where there is no tty for `sudo -v`). +if [ "$(id -u)" -eq 0 ]; then + SUDO="" +else + SUDO="sudo" + # Prompt for password upfront + sudo -v +fi # The pool starts at 127.0.0.2 by default (127.0.0.1 is left for other services on localhost). # Override via the HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START environment variable. @@ -35,7 +42,20 @@ fi END=$((START + COUNT - 1)) for i in $(seq $START $END); do - sudo ifconfig lo0 alias 127.0.0.$i up + # Use a host (/32) netmask, not the implicit class-A /8. Without an explicit netmask, + # macOS gives each 127.0.0.x alias a 255.0.0.0 mask, so every alias claims to own the + # entire 127.0.0.0/8 network. With a large COUNT that means dozens/hundreds of interface + # addresses all asserting the same subnet, which drives mDNSResponder's address-conflict + # defense (PacketRRConflict) into an O(n^2) storm — pinning a CPU core and flooding + # 5353 with loopback announcements. A /32 host route removes the subnet overlap: each + # alias is an isolated host, so there is no conflict cascade even at the full 254-address + # pool. (Measured: 254 aliases /8 => ~65% CPU; 254 aliases /32 => ~0% CPU.) + # + # Remove any pre-existing alias first so re-running this converts a machine previously + # configured with the old /8 aliases; ifconfig alias on an existing address does not + # reliably reset its netmask. The `-alias` is a harmless no-op if the address is absent. + $SUDO ifconfig lo0 -alias 127.0.0.$i 2>/dev/null + $SUDO ifconfig lo0 alias 127.0.0.$i netmask 255.255.255.255 up done echo "✓ Configured $COUNT loopback addresses (127.0.0.$START-127.0.0.$END)" \ No newline at end of file