TapConnection

This process's identity at the server: every Device it attaches belongs to it and is detached with it — explicitly by close, or by the server when the process goes away. One per process is the norm; see TapClient.connect.

Liveness is an explicitly owned observeScope: a SupervisorJob + Dispatchers.IO scope that lives exactly as long as this connection. observe collects the server-streaming Observe Flow in that scope and waits for the first event before returning, so the stream is established before any AttachDevice. When the scope is cancelled by close (or the process dies) the server notices the dropped stream and detaches every device of this connection. Never GlobalScope: close sends Disconnect first, then cancels and joins the collection.

Lifecycle is linearized: exactly one Observe is ever started (observe fails on a second call); an empty stream or any failure before the first event fails the connect and closes the newly opened id under a bounded non-cancellable context. Any normal completion or failure of the stream after the first event is unexpected: the connection is atomically marked unusable (the terminal cause is retained, never swallowed), further attachDevice calls and every admitted Device operation are rejected locally, and registered device handles are marked invalid. close is single-flight: concurrent and repeated callers share one Disconnect RPC (60 s gRPC deadline, mapped) and one bounded non-cancellable teardown that cancels and joins the collector even for a stubborn (cancellation-ignoring) collector, preserving the primary Disconnect failure and suppressing cleanup failures.

Properties

Link copied to clipboard
Link copied to clipboard
val id: String
Link copied to clipboard

True once close started.

Link copied to clipboard

True once the stream terminated unexpectedly after establishment (or was never usable).

Link copied to clipboard

True while the connection can still attach devices: neither closed nor isInvalid.

Link copied to clipboard

The liveness stream's events so far (diagnostics, last 200): observing and closing: <reason>; heartbeats are not recorded.

Functions

Link copied to clipboard
suspend fun <R> attach(serial: String, timeouts: Timeouts = Timeouts(), options: DeviceOptions = DeviceOptions(), block: suspend (Device) -> R): R

Attaches serial (attachDevice), runs block with the device and always detaches it afterwards. Runs inside the caller's tapTest / tapScope, or installs a TapContext of its own when there is none, so a script needs no tapScope. A block failure wins and a detach failure after it is added as suppressed; when block succeeds, a detach failure (including quarantine) is thrown.

Link copied to clipboard
suspend fun attachDevice(serial: String, timeouts: Timeouts = Timeouts(), options: DeviceOptions = DeviceOptions()): Device

Attach serial. The underlying device session holds the device's per-serial lock until Device.detach; if another session holds it, attachment fails with DeviceBusyException — at once, or after DeviceOptions.waitForDevice. Rejected locally without an RPC once the connection is closed or its liveness stream ended unexpectedly.

Link copied to clipboard
suspend fun availableSerials(): List<String>

Serials a test can use, from TapClient.devices: online and not quarantined, free ones first, then ones another session holds (opening then waits, see DeviceOptions.waitForDevice). Exclusive use is enforced by the session itself, so there is nothing to acquire beforehand.

Link copied to clipboard
suspend fun close()

Closes explicitly (recorded as a client request), then drops the liveness stream. Single-flight and idempotent: concurrent and repeated callers share one Disconnect RPC (60 s gRPC deadline, mapped, under a longer outer bound that maps distinctly) and one shared completion that resolves only after the RPC plus the bounded observe collector/scope teardown. The close transition is atomic under stateMutex: the shared completion is published and closing is marked in one critical section, so no admission passes after close ownership exists. Every caller awaits the same full outcome and rethrows the same primary failure; a cleanup error becomes primary only when there is no Disconnect error, otherwise it is suppressed — never swallowed silently. If observation already dropped, the server has detached the devices and Disconnect may report unknown-connection (mapped); the scope is still cleaned up exactly once.

Link copied to clipboard
suspend fun <R> use(block: suspend (TapConnection) -> R): R

Runs block with this connection, then closes it. A block failure wins; a close failure after it is added as suppressed.