Summary
The macOS Tart runner can get stuck cycling runners because waitForSSH reports SSH not reachable for every VM, even though the VM receives an IP and can be reached manually from the host in some attempts.
This currently blocks the Integration Test (Tart) job on PR #71:
Observed behavior
On eiu-the-runner, elastic-fruit-runner repeatedly prepares a Tart VM, gets 192.168.64.3, waits for SSH for two minutes, then deletes the VM and retries.
Representative log lines:
preparing runner runnerSet=fruit-runner-macos-arm64 runner=fruit-runner-macos-arm64-d9024
VM image not found locally, pulling image=ghcr.io/cirruslabs/macos-tahoe-xcode:latest
cloning VM image=ghcr.io/cirruslabs/macos-tahoe-xcode:latest name=fruit-runner-macos-arm64-d9024
starting VM name=fruit-runner-macos-arm64-d9024
waiting for VM IP name=fruit-runner-macos-arm64-d9024
waiting for SSH name=fruit-runner-macos-arm64-d9024 ip=192.168.64.3 retry_in=1s
...
start runner failed runnerSet=fruit-runner-macos-arm64 runner=fruit-runner-macos-arm64-d9024 err="start runner in VM fruit-runner-macos-arm64-d9024: SSH not reachable on fruit-runner-macos-arm64-d9024 (192.168.64.3:22) after 2m0s"
stopping VM name=fruit-runner-macos-arm64-d9024
deleting VM name=fruit-runner-macos-arm64-d9024
This repeats across multiple runner names, so it does not look like a single corrupt VM instance.
Profiling / debugging notes
Sampling the live daemon did not show CPU pressure or an internal hot loop. Most threads were parked in Go runtime waits, kevent, or read, which matches the process waiting on external VM/network readiness.
The installed Homebrew binary is stripped, so sample and go tool addr2line could not map addresses back to Go symbols. This makes live production profiling harder than necessary.
Manual checks from the same host during the failure window showed that the Tart VM and credentials can be valid:
sshpass -p admin ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 admin@192.168.64.3 "echo ok && sw_vers -productVersion"
ok
26.3
A tcpdump captured a SYN toward the VM SSH port with the physical LAN source address instead of the Tart bridge source address:
IP 192.168.1.87.<ephemeral> > 192.168.64.3.22: Flags [S]
The expected host-side Tart bridge address is 192.168.64.1 on bridge101; route -n get 192.168.64.3 also reported bridge101.
Suspected root cause
internal/tart/manager.go currently treats SSH readiness as a raw TCP probe:
dialer := net.Dialer{Timeout: 2 * time.Second}
conn, err := dialer.DialContext(ctx, "tcp", ip+":22")
if err == nil {
conn.Close()
return nil
}
That probe can falsely fail in this macOS/Tart bridge networking setup. Because the retry log only records name, ip, and retry_in, we lose the actual dial error, local address, and interface information needed to distinguish slow boot, route selection, firewall/refused, and timeout cases.
There is also a behavioral mismatch: after waitForSSH succeeds, the next step uses sshpass ssh. In the observed environment, manual sshpass ssh can succeed while the daemon continues to fail the raw TCP readiness gate.
Proposed fix
- Log the actual
DialContext error on every readiness retry, or at least on timeout.
- On successful TCP probe, log
conn.LocalAddr() and conn.RemoteAddr() at debug/info level.
- Prefer making readiness use the same transport as the real operation, for example
sshpass ssh -o ConnectTimeout=... admin@<ip> true, instead of a bare TCP port check.
- If keeping TCP probing, consider explicitly selecting/binding the Tart bridge local address/interface for VM IPs.
- Add an ops/debug mode to preserve failed Tart VMs instead of deleting them immediately after readiness timeout.
- Publish or retain symbolized debug builds for the runner binary used on self-hosted machines so production samples can be mapped back to Go code.
Increasing the two-minute timeout may help slow boots, but it does not address the false-negative readiness signal seen here.
Acceptance criteria
- A Tart VM that is reachable by
sshpass ssh admin@<ip> true is accepted as ready by the runner manager.
- Readiness timeout logs include the underlying connection error and enough local/remote address data to diagnose routing/interface issues.
- The
Integration Test (Tart) job no longer remains queued because the runner repeatedly fails during VM SSH readiness.
Summary
The macOS Tart runner can get stuck cycling runners because
waitForSSHreportsSSH not reachablefor every VM, even though the VM receives an IP and can be reached manually from the host in some attempts.This currently blocks the
Integration Test (Tart)job on PR #71:Observed behavior
On
eiu-the-runner,elastic-fruit-runnerrepeatedly prepares a Tart VM, gets192.168.64.3, waits for SSH for two minutes, then deletes the VM and retries.Representative log lines:
This repeats across multiple runner names, so it does not look like a single corrupt VM instance.
Profiling / debugging notes
Sampling the live daemon did not show CPU pressure or an internal hot loop. Most threads were parked in Go runtime waits,
kevent, orread, which matches the process waiting on external VM/network readiness.The installed Homebrew binary is stripped, so
sampleandgo tool addr2linecould not map addresses back to Go symbols. This makes live production profiling harder than necessary.Manual checks from the same host during the failure window showed that the Tart VM and credentials can be valid:
A tcpdump captured a SYN toward the VM SSH port with the physical LAN source address instead of the Tart bridge source address:
The expected host-side Tart bridge address is
192.168.64.1onbridge101;route -n get 192.168.64.3also reportedbridge101.Suspected root cause
internal/tart/manager.gocurrently treats SSH readiness as a raw TCP probe:That probe can falsely fail in this macOS/Tart bridge networking setup. Because the retry log only records
name,ip, andretry_in, we lose the actual dial error, local address, and interface information needed to distinguish slow boot, route selection, firewall/refused, and timeout cases.There is also a behavioral mismatch: after
waitForSSHsucceeds, the next step usessshpass ssh. In the observed environment, manualsshpass sshcan succeed while the daemon continues to fail the raw TCP readiness gate.Proposed fix
DialContexterror on every readiness retry, or at least on timeout.conn.LocalAddr()andconn.RemoteAddr()at debug/info level.sshpass ssh -o ConnectTimeout=... admin@<ip> true, instead of a bare TCP port check.Increasing the two-minute timeout may help slow boots, but it does not address the false-negative readiness signal seen here.
Acceptance criteria
sshpass ssh admin@<ip> trueis accepted as ready by the runner manager.Integration Test (Tart)job no longer remains queued because the runner repeatedly fails during VM SSH readiness.