Skip to content

The probes ​

This is the one thing to know before the rest of it. A check is a socket — a resolve, a connect, an HTTP request, an ICMP echo — and this port owns none. So a diagnosis asks the app for the network, one probe at a time, and what the app answers is what the report records:

the checkwhat it asks forwhat you answer
pingthe host, a timeout in secondshow long it took, and what share of the pings were lost
dnsthe domain, a timeout in msthe addresses it resolved to
tcpthe ip and port, a timeout in mswhether the noop went out, what came back, and whether it was the answer to it
httpthe URL of the net-check CGIthe HTTP status and how long the request took

Which is what makes a diagnosis testable, and what lets a host that is not a phone run one at all: the same closure that answers on a device answers from a test, or from a machine with no radio.

What the app hands in ​

your app iswhat the probes are asked of
Rustthe Ask of sdt.run_checks(&mut ask, net)
iOS / watchOS, Swift or Objective-Cthe closure of MarsSdt.runChecks(networkType:) { … }, or the block it becomes
Androidthe SdtLogic.IProbe of SdtLogic.runChecks(net, probe)
Kotlin Multiplatformthe same IProbe, of SdtLogic.runChecks(1, probe)
anything with a C FFIthe function pointer of mars_sdt_run_checks(ctx, probe, net)
HarmonyOSthe same function pointer, through a NAPI shim of your own

The run asks them one at a time, in the order of the plan, on the thread that called it — and the call does not come back until every probe has answered, so no sleep and no loop is involved the way one is for a task. A probe that has nothing to say answers "nothing" — .nothing() in Swift, ProbeAnswer.None in the shared Kotlin, MarsSdtNothing in C — and the check that asked is recorded as one that failed, which ends the run: what stands behind it in the plan is not checked. A ping is the one exception — a ping nobody sent is a check that did not run, so it is left out of the report and the run goes on behind it.

A probe is asked while the port holds the diagnosis, so it must not call back into it from inside its answer — not to start a second run, not to ask whether one is in flight: what it asked would wait for the run that is asking it, on the thread that is running it. Everything a check needs comes with the question.

The answers ​

the checkRustSwiftshared KotlinAndroidC
pingAnswer::Ping { error_code, rtt, status }.ping(errorCode:rtt:lossRate:averageRTT:)ProbeAnswer.PingSdtLogic.Answer.ping(…)MarsSdtPing
dnsAnswer::Dns { error_code, rtt, local_dns, ips }.dns(errorCode:rtt:addresses:)ProbeAnswer.DnsSdtLogic.Answer.dns(…)MarsSdtDns
tcpAnswer::Tcp { sent, received, is_noop_resp, conntime, rtt }.tcp(errorCode:rtt:noop:)ProbeAnswer.TcpSdtLogic.Answer.tcp(…)MarsSdtTcp
httpAnswer::Http { error_code, status_code, rtt }.http(errorCode:rtt:statusCode:)ProbeAnswer.HttpSdtLogic.Answer.http(…)MarsSdtHttp

The four are one struct in C — MarsSdtAnswer — and the kind in it is which one it is: the names of the last column are the four MarsSdtKind values and not four types. Two of the Rust's fields are the Rust's alone — local_dns and conntime, which no other seam carries a field for — so a probe that answers on one of them leaves the report's localDns empty and its conntime at 0.

An answer of a probe that is not the one asked is not the answer to it: what the report records is what came back for the check that was run.

Where to go next ​

  • The checks — the mode, and the plan the run walks in order.
  • The report — the JSON of what the probes answered.

MIT, like the upstream project.