Skip to content

Releases: gewill/PingKit

PingKit 0.6.2

Choose a tag to compare

@gewill gewill released this 29 Aug 12:06

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:

  • PingError separates setup failures, which throw for every run, from
    per-probe network outcomes, which reach a continuous run as
    PingResponse events and become a PingError only through the
    one-shot ping(_:).
  • PingStatistics.lost folds three fates into one number — timeouts,
    ICMP errors, and probes the socket refused to send — because only a
    matched echo reply increments received.

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

Choose a tag to compare

@gewill gewill released this 14 Jul 15:34

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

Choose a tag to compare

@gewill gewill released this 14 Jul 14:36

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) .sent followed 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: Pinger no longer finishes the stream on a send
    error. The failed probe isn't counted in transmitted, 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 throws PingError.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 to ping(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

Choose a tag to compare

@gewill gewill released this 14 Jul 04:20

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 via recvmsg.
  • PingConfiguration.AddressFamily.automatic (follows the system's
    getaddrinfo ordering, 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 typed PingResponse events.
  • On Darwin the socket sets ICMP6_FILTER so it only wakes for the
    message types ping consumes (not NDP/RA multicast).
  • Tracer remains IPv4-only in this release.

Outgoing TTL / hop limit (was 0.4.0)

  • PingConfiguration.timeToLive (1...255; nil keeps the system default),
    applied as IPv4 TTL or IPv6 unicast hop limit.
  • CLI -m sets 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-based pingkit command with ping / trace
    subcommands, typed validation, and generated help. -4 / -6 force the
    address family. The dependency links only into the CLI target; the
    PingKit library stays zero-dependency.

Breaking changes

  • PingReply.from is now IPAddress (was IPv4Endpoint).
  • PingResponse gains .sent, .packetTooBig, and .parameterProblem
    cases — exhaustive switches must handle them.
  • The executable product is named pingkit-cli (the command is still
    pingkit); the old ping-cli product is removed.

Notes

  • unreachable(code:) codes are numbered differently by IPv4 vs IPv6;
    interpret code according 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

Choose a tag to compare

@gewill gewill released this 13 Jul 14:41

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, and Pinger.HostResolver are 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.responsesAlreadyConsumed renamed to sequenceAlreadyConsumed: the single-consumer error now applies uniformly to Pinger.responses and Tracer.hops, with a message that no longer wrongly refers to Pinger when thrown by Tracer.

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

Choose a tag to compare

@gewill gewill released this 13 Jul 06:41

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

Choose a tag to compare

@gewill gewill released this 13 Jul 04:43

First release: IPv4 ICMP ping for Swift over unprivileged datagram sockets.

Features

  • Pinger actor with an AsyncSequence API (for try await response in pinger.responses), plus one-shot Pinger.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-cli demo 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