Releases: gewill/PingKit
Release list
PingKit 0.6.2
PingKit 0.6.2 is a documentation release. There is no API or behavior
change: every .swift change under Sources since 0.6.1 is a ///
comment.
Documentation
The DocC catalog grew from a single landing page to a landing page plus
two articles — Getting Started (installation through the lifecycle
contract and statistics) and Platform Notes (address families and
NAT64, the iOS local-network prompt and backgrounding, the Linux
ping_group_range requirement and identifier rewriting). Those platform
behaviors previously lived only in the README, invisible to anyone
reading the generated documentation.
Public symbol coverage, measured from the generated archive, went from
75% to 88%, and every public type now carries a summary. Two of the new
summaries record semantics that were not written down anywhere:
PingErrorseparates setup failures, which throw for every run, from
per-probe network outcomes, which reach a continuous run as
PingResponseevents and become aPingErroronly through the
one-shotping(_:).PingStatistics.lostfolds three fates into one number — timeouts,
ICMP errors, and probes the socket refused to send — because only a
matched echo reply incrementsreceived.
Swift Package Index
The repository is now public and submitted to the index
(SwiftPackageIndex/PackageList#15025). Upstream has paused new package
ingestion while they update infrastructure, so the request is queued.
The Swift version and platform badges added to the README read
"pending" until that completes, then fill in on their own.
Test
duplicateReplyCountedOnce raced a 1 ms polling task against a 50 ms
probe timeout and failed intermittently on the CI iOS Simulator. The
duplicate reply is now delivered inline from send through a new mock
hook; the assertions are unchanged, so a Pinger that counted
duplicates would still fail the test.
Verified
72 tests pass on macOS, the iOS Simulator, and Linux Swift 6.0 / 6.1.
DocC builds clean with --warnings-as-errors.
PingKit 0.6.1
PingKit 0.6.1 fixes how send failures are counted so they show up as
packet loss, matching ping(8).
Fix
0.6.0 excluded send failures from transmitted, so a mid-run network
drop showed 0% loss in statistics() — the outage vanished from the
aggregate for consumers that only read the summary.
A failed send now counts toward transmitted (not received), so it
surfaces as packet loss, the same way ping(8) does. The .sendFailed
event still carries the errno, so consumers can still tell "never
sent" from "sent but lost".
This was motivated by a live-disconnect capture of system ping(8): the
sendto: No route to host probes stayed in the transmit tally, ending
26 transmitted / 17 received / 34.6% loss.
Impact
Behavior change, no API break. Same disconnect scenario:
- 0.6.0 →
17 transmitted, 0% loss - 0.6.1 → failed probes counted, loss reflects the real gap
Verified: pingkit-cli 8.8.8.8 -6 -c 3 now reports
3 transmitted, 0 received, 100% loss (exit 2). 72 tests pass on macOS,
iOS Simulator, and Linux Swift 6.0 / 6.1.
PingKit 0.6.0
PingKit 0.6.0 makes send failures non-fatal. A probe the socket rejects
(e.g. an IPv4 literal forced onto an IPv6-only network, or a briefly
unreachable interface) no longer aborts the whole run — it surfaces as a
.sendFailed event and the session recovers on the next interval.
This is a breaking minor: PingResponse gains a case.
New invariant
Each sequence produces exactly one of two outcomes:
- (a)
.sentfollowed by a terminal event (.reply,.timeout,
.unreachable,.timeExceeded,.packetTooBig,.parameterProblem) —
the socket accepted the probe; or - (b) a single
.sendFailed(sequence:errno:)with no.sent— the
probe never left the socket.
Changes
PingResponse.sendFailed(sequence: UInt16, errno: Int32)— new case.- Non-fatal send path:
Pingerno longer finishes the stream on a send
error. The failed probe isn't counted intransmitted, gets no pending
entry and no.sent; the run continues and recovers when the network does.
A bounded.times(n)run still terminates (the failed slot completes). - One-shot
ping(_:)still throwsPingError.sendFailed(errno:). - Statistics: failed sends are not counted toward
transmitted. - CLI: prints
Send failed for icmp_seq N (errno …)per failure and
exits non-zero when nothing came back — whether probes were sent and lost
or never left the socket at all — closer toping(8).
Breaking change
PingResponse gains .sendFailed; exhaustive switches must handle it.
Notes
Verified on macOS, iOS Simulator, and Linux Swift 6.0 / 6.1 (72 tests).
End-to-end: pingkit-cli 8.8.8.8 -6 (IPv4 literal forced to IPv6, errno 22)
now reports a send failure per probe and runs to completion instead of
aborting.
PingKit 0.5.0
PingKit 0.5.0 adds dual-stack IPv6 ping and a formal pingkit CLI, and
folds in the previously unreleased 0.4.0 work (outgoing TTL + .sent
events). This is a breaking minor: see below before upgrading.
Highlights
IPv6 ping (dual-stack)
- Full ICMPv6 echo over unprivileged datagram sockets, with hop-limit
ancillary data and source address viarecvmsg. PingConfiguration.AddressFamily—.automatic(follows the system's
getaddrinfoordering, so hostnames resolve over DNS64/NAT64),.ipv4,
or.ipv6. IPv6 literals and scoped link-local addresses are supported.- ICMPv6 Destination Unreachable, Time Exceeded, Packet Too Big, and
Parameter Problem map to typedPingResponseevents. - On Darwin the socket sets
ICMP6_FILTERso it only wakes for the
message types ping consumes (not NDP/RA multicast). Tracerremains IPv4-only in this release.
Outgoing TTL / hop limit (was 0.4.0)
PingConfiguration.timeToLive(1...255;nilkeeps the system default),
applied as IPv4 TTL or IPv6 unicast hop limit.- CLI
-msets the outgoing TTL in ping mode.
.sent probe events (was 0.4.0)
PingResponse.sent(sequence:)fires as each echo request leaves the
socket, before that sequence's terminal event — so a UI can insert a
pending row per probe and update it in place.
Formal CLI
swift-argument-parser-basedpingkitcommand withping/trace
subcommands, typed validation, and generated help.-4/-6force the
address family. The dependency links only into the CLI target; the
PingKitlibrary stays zero-dependency.
Breaking changes
PingReply.fromis nowIPAddress(wasIPv4Endpoint).PingResponsegains.sent,.packetTooBig, and.parameterProblem
cases — exhaustiveswitches must handle them.- The executable product is named
pingkit-cli(the command is still
pingkit); the oldping-cliproduct is removed.
Notes
unreachable(code:)codes are numbered differently by IPv4 vs IPv6;
interpretcodeaccording to the address family the run resolved to.- Verified on macOS, iOS Simulator (including ICMPv6 loopback integration
tests), and Linux Swift 6.0 / 6.1.
PingKit 0.3.0
New
Receive ordering guarantee
Socket receive callbacks now flow through a single AsyncStream pipeline consumed by one task, so replies reach Pinger/Tracer in socket delivery order. Previously each datagram was dispatched in its own unstructured Task, which could reorder bursts of replies before they reached the actor. A 100-reply burst regression test covers the new behavior.
Breaking (0.x)
- Public API narrowed:
PingSocket,MonotonicTimestamp,Pinger.SocketFactory, andPinger.HostResolverare now internal. They existed only for internal test injection; a redesigned, stable socket-injection API is a 0.4 candidate driven by real-world demand (see PLAN.md §6.3). PingError.responsesAlreadyConsumedrenamed tosequenceAlreadyConsumed: the single-consumer error now applies uniformly toPinger.responsesandTracer.hops, with a message that no longer wrongly refers toPingerwhen thrown byTracer.
CI
The Linux matrix now also verifies Swift 6.0, keeping the declared minimum toolchain continuously tested alongside 6.1.
Known limitations
Unchanged from 0.2.0: on Linux, ICMP errors go to the socket error queue (MSG_ERRQUEUE), which PingKit doesn't read yet — traceroute intermediate hops show as timeouts there.
🤖 Generated with Claude Code
PingKit 0.2.0
New
ICMP traceroute
Tracer sends echo requests with increasing TTL on the same unprivileged ICMP socket and matches Time Exceeded answers from intermediate routers:
for try await hop in Tracer(host: "8.8.8.8").hops { ... }
let route = try await Tracer.trace("8.8.8.8")One TracerouteHop per TTL groups the configurable per-hop probes; the trace ends at the destination's echo reply, a Destination Unreachable, or maxHops. Same single-consumer / cancellation / stop() lifecycle as Pinger. CLI: ping-cli trace <host>. Verified live against 8.8.8.8 (15-hop route).
Kernel receive timestamps
RTTs on Apple platforms are now measured against the kernel's packet-arrival timestamps (SO_TIMESTAMP_MONOTONIC), removing scheduler wakeup latency — measured ~0.18 ms tighter on loopback (avg 0.34 ms → 0.10 ms, stddev 0.13 → 0.02 ms).
Breaking (0.x)
PingSocket.activate now delivers a MonotonicTimestamp instead of ContinuousClock.Instant, and gained setTimeToLive(_:). Only affects custom PingSocket implementations; the Pinger/Tracer API is unchanged.
Known limitations
On Linux, ICMP errors go to the socket error queue (MSG_ERRQUEUE), which PingKit doesn't read yet — traceroute intermediate hops show as timeouts there.
🤖 Generated with Claude Code
PingKit 0.1.0
First release: IPv4 ICMP ping for Swift over unprivileged datagram sockets.
Features
Pingeractor with anAsyncSequenceAPI (for try await response in pinger.responses), plus one-shotPinger.ping(_:)- Configurable interval, timeout, probe count, and payload size
- ICMP error mapping: Destination Unreachable and Time Exceeded are reported per-probe instead of being silently dropped (including a workaround for XNU byte-swapping the quoted IP header of error messages)
ping(8)-style statistics: transmitted/received/loss, min/avg/max/stddev RTT- Strict lifecycle semantics: single consumer, cancellation-aware teardown, idempotent
stop() - Zero runtime dependencies; Swift 6 strict concurrency throughout
ping-clidemo executable- CI-verified on macOS and Linux (real loopback pings on both)
Platforms
macOS 13+ / iOS 16+; Linux best-effort (requires net.ipv4.ping_group_range).
🤖 Generated with Claude Code