diff --git a/.agents/skills/cryptad-architecture/SKILL.md b/.agents/skills/cryptad-architecture/SKILL.md index eaffa1c2626..220310934d7 100644 --- a/.agents/skills/cryptad-architecture/SKILL.md +++ b/.agents/skills/cryptad-architecture/SKILL.md @@ -13,7 +13,7 @@ Use this skill when you need to: - Understand request routing, updates, plugins, or storage. - Make changes that could affect wire compatibility or on-disk formats. -## Build/module layout (PR-1) +## Build/module layout (current) - Cryptad now uses a partial multi-project Gradle build. - The root project `:cryptad` remains the daemon/application project. - The root project still owns `buildJar`, `run`, `runLauncher`, distribution/jpackage tasks, the @@ -21,10 +21,22 @@ Use this skill when you need to: - Leaf subprojects: - `:foundation-support` → the current stable generic subset of `network.crypta.support`, `network.crypta.support.api`, `network.crypta.support.io`, - `network.crypta.support.compress`, `network.crypta.support.math`, plus - `network.crypta.node.FSParseException` + `network.crypta.support.compress`, `network.crypta.support.math`, + `network.crypta.support.transport.ip`, plus `network.crypta.io.AddressIdentifier`, + `network.crypta.io.WritableToDataOutputStream`, `network.crypta.node.FSParseException`, + `network.crypta.node.FastRunnable`, and `network.crypta.node.SemiOrderedShutdownHook` - `:foundation-store-contracts` → neutral `network.crypta.store` contracts - `BlockMetadata`, `GetPubkey`, `StorableBlock` + `BlockMetadata`, `GetPubkey`, `StorableBlock`, plus the `network.crypta.store.alerts` seam + (`StoreAlertSink`, `StoreMaintenanceAlertKind`, `StoreMaintenanceAlertSource`) + - `:foundation-crypto-keys` → `network.crypta.crypt`, `network.crypta.keys`, plus + `network.crypta.support.io.BucketTools` and + `network.crypta.support.io.PrependLengthOutputStream` + - `:foundation-store` → reusable `network.crypta.store` implementations plus + `network.crypta.store.caching` and `network.crypta.store.saltedhash` + - `:interop-wire` → the leaf-safe wire/message/schema/version/probe nucleus: + selected `network.crypta.io.comm` types, `network.crypta.node.Version`, + `network.crypta.node.probe.Error`, `network.crypta.node.probe.Type`, and + `network.crypta.support.Serializer` - `:foundation-config` → `network.crypta.config`, `network.crypta.l10n`, and the main l10n resources; public config APIs re-export `:foundation-support` and `:foundation-fs` where they expose shared types @@ -47,10 +59,16 @@ Use this skill when you need to: `LegacyFirstTimeWizardPort`, `LegacyToadletSymlinkPort`, `LegacyWelcomePagePort`, and `LegacyWelcomeActionPort`. - The large cyclic daemon core still lives in the root project: - `network.crypta.node` (except the transitional `FSParseException` move), - `network.crypta.io`, `network.crypta.client`, `network.crypta.clients`, - the remaining daemon-coupled support code, `network.crypta.crypt`, `network.crypta.keys`, - most store implementations under `network.crypta.store`, and `network.crypta.tools`. + most of `network.crypta.node`, the daemon-coupled transport/socket/filter side of + `network.crypta.io.comm`, `network.crypta.client`, `network.crypta.clients`, + `network.crypta.node.runtime`, the remaining daemon-coupled support code, and + `network.crypta.tools`. +- The wire split is intentionally narrow: + `:interop-wire` owns the message/schema nucleus, while root keeps `MessageCore`, + `MessageFilter`, `AsyncMessageFilterCallback`, `SlowAsyncMessageFilterCallback`, + `PeerContext`, incoming-packet filters, socket handlers, and statistics collection. + `network.crypta.io.comm.Message` now depends on the minimal `MessageSource` seam instead of + directly on `PeerContext`. - `:foundation-config` is the current home for all main `network.crypta.config` and `network.crypta.l10n` sources. Their unit tests still live in the root test tree and are run by the root project. @@ -74,19 +92,35 @@ Use this skill when you need to: - Storage abstractions: `FreenetStore` - CHK/SSK stores: `CHKStore`, `SSKStore` - Caching: `SlashdotStore` -- Neutral contracts `BlockMetadata`, `GetPubkey`, and `StorableBlock` now live in - `:foundation-store-contracts`; store implementations still remain in the root project. +- `:foundation-store` now owns the reusable store implementations, cache layer, and salted-hash + store code. +- Neutral contracts `BlockMetadata`, `GetPubkey`, and `StorableBlock` live in + `:foundation-store-contracts`, along with the store-maintenance alert seam in + `network.crypta.store.alerts`. +- Root-owned runtime/UI integration remains in `network.crypta.node.runtime`, for example + `UserAlertManagerStoreAlertSink`. ### Cryptography (`network.crypta.crypt`) - Encryption: block cipher / AES streams - Signatures: DSA/ECDSA - Hashing: SHA-256 and others - RNG: `RandomSource` / Yarrow +- This package now lives in `:foundation-crypto-keys`. ### Key management (`network.crypta.keys`) - Client keys: `ClientCHK`, `ClientSSK` - URIs: `FreenetURI` - Updatable keys: USK +- This package now lives in `:foundation-crypto-keys`. + +### Wire/message nucleus (`network.crypta.io.comm`, `network.crypta.node.Version`) +- `:interop-wire` owns the leaf-safe message/schema/address subset such as `Message`, + `MessageType`, `Peer`, `FreenetInetAddress`, and related exceptions. +- `:interop-wire` also owns `network.crypta.node.Version`, + `network.crypta.node.VersionParseException`, `network.crypta.node.probe.Error`, + `network.crypta.node.probe.Type`, and `network.crypta.support.Serializer`. +- Root keeps transport-facing code such as `PeerContext`, `MessageCore`, filters, packet/socket + handlers, and runtime helpers like `network.crypta.node.runtime.SSL`. ### Client APIs - High-level client: `network.crypta.client` @@ -194,7 +228,11 @@ Use this skill when you need to: - Logging, data structures, threading, helpers - `:foundation-support` now owns the stable generic support subset across `network.crypta.support`, `network.crypta.support.api`, `network.crypta.support.io`, - `network.crypta.support.compress`, and `network.crypta.support.math`. + `network.crypta.support.compress`, `network.crypta.support.math`, and + `network.crypta.support.transport.ip`. +- `:foundation-support` also owns `network.crypta.io.AddressIdentifier`, + `network.crypta.io.WritableToDataOutputStream`, `network.crypta.node.FastRunnable`, + `network.crypta.node.SemiOrderedShutdownHook`, and `network.crypta.support.SerializationLimits`. - The root project still owns daemon-coupled support code and higher-level wiring that is not yet stable enough to extract cleanly. @@ -205,8 +243,14 @@ Use this skill when you need to: ### Foundation leaf modules - `:foundation-support`: stable generic `network.crypta.support*` subset plus - `network.crypta.node.FSParseException` -- `:foundation-store-contracts`: neutral `network.crypta.store` contracts + `network.crypta.io.AddressIdentifier`, `network.crypta.io.WritableToDataOutputStream`, + `network.crypta.node.FSParseException`, `network.crypta.node.FastRunnable`, and + `network.crypta.node.SemiOrderedShutdownHook` +- `:foundation-store-contracts`: neutral `network.crypta.store` contracts plus + `network.crypta.store.alerts` +- `:foundation-crypto-keys`: `network.crypta.crypt`, `network.crypta.keys` +- `:foundation-store`: reusable `network.crypta.store` implementations +- `:interop-wire`: wire/message/schema/version/probe nucleus - `:foundation-config`: `network.crypta.config`, `network.crypta.l10n` - `:foundation-fs`: `network.crypta.fs` - `:foundation-compat`: `network.crypta.compat` @@ -250,7 +294,8 @@ Use this skill when you need to: ## Versioning system - A single integer build number is set in `build.gradle.kts` (`version = ""`). -- Version tokens are replaced into `network/crypta/node/Version.java` during build (`@build_number@`, `@git_rev@`). +- Version tokens are replaced into the `:interop-wire` `network/crypta/node/Version.java` + template during build (`@build_number@`, `@git_rev@`). - Version strings support both Cryptad and Fred formats; compatibility enforces protocol match and minimum builds. - Freenet interop uses historical identifiers (e.g., `"Fred,0.7"`) for wire compatibility where applicable. - Core update descriptors (`core-info.json`) must publish `version` as an integer string; this value diff --git a/.agents/skills/cryptad-build-test/SKILL.md b/.agents/skills/cryptad-build-test/SKILL.md index b7bf06739b5..5e10bc41463 100644 --- a/.agents/skills/cryptad-build-test/SKILL.md +++ b/.agents/skills/cryptad-build-test/SKILL.md @@ -18,15 +18,27 @@ Use this skill when you need to: ## Build layout - Cryptad now uses a partial multi-project Gradle build. - Use root-project tasks by default; the root project remains the daemon/application target. -- Current leaf projects are `:foundation-support`, `:foundation-store-contracts`, +- Current leaf projects are `:foundation-support`, `:foundation-store`, + `:foundation-store-contracts`, `:foundation-crypto-keys`, `:interop-wire`, `:foundation-config`, `:foundation-fs`, `:foundation-compat`, `:runtime-spi`, `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. - The extracted leaf projects compile separately, but `buildJar`, `run`, `runLauncher`, `assembleCryptadDist`, and jpackage tasks are still rooted at `:cryptad`. - `:foundation-support` owns the current stable generic support subset under - `network.crypta.support*` plus `network.crypta.node.FSParseException`. + `network.crypta.support*`, `network.crypta.support.transport.ip`, + `network.crypta.io.AddressIdentifier`, `network.crypta.io.WritableToDataOutputStream`, + `network.crypta.node.FSParseException`, `network.crypta.node.FastRunnable`, and + `network.crypta.node.SemiOrderedShutdownHook`. - `:foundation-store-contracts` owns the neutral `network.crypta.store` contracts - `BlockMetadata`, `GetPubkey`, and `StorableBlock`. + `BlockMetadata`, `GetPubkey`, and `StorableBlock`, plus the `network.crypta.store.alerts` + seam. +- `:foundation-crypto-keys` owns `network.crypta.crypt`, `network.crypta.keys`, and the adjacent + `BucketTools` / `PrependLengthOutputStream` helpers. +- `:foundation-store` owns reusable `network.crypta.store` implementations plus + `network.crypta.store.caching` and `network.crypta.store.saltedhash`. +- `:interop-wire` owns the narrow wire/message/schema/version/probe nucleus: + leaf-safe `network.crypta.io.comm` message/schema classes, `network.crypta.node.Version`, + `network.crypta.node.probe.Error` and `Type`, and `network.crypta.support.Serializer`. - `:foundation-config` owns the main `network.crypta.config` and `network.crypta.l10n` sources. Its public APIs now re-export `:foundation-support` and `:foundation-fs` where needed. - Every extracted internal leaf must keep leaf-owned aggregated-output metadata in sync at @@ -72,9 +84,17 @@ When running ./gradlew test via OpenCode bash, set timeout ≥ 15 minutes (≥ 9 - `./gradlew compileJava` - Compile the support leaf when you touched extracted generic support classes: - `./gradlew :foundation-support:classes` +- Compile the crypto/keys leaf when you touched `network.crypta.crypt`, `network.crypta.keys`, + or the moved bucket/length helpers: + - `./gradlew :foundation-crypto-keys:classes` +- Compile the reusable store leaf when you touched extracted `network.crypta.store`, + `network.crypta.store.caching`, or `network.crypta.store.saltedhash` code: + - `./gradlew :foundation-store:compileJava` - Compile the neutral store-contracts leaf when you touched `BlockMetadata`, `GetPubkey`, or - `StorableBlock`: + `StorableBlock`, or the store-maintenance alert seam: - `./gradlew :foundation-store-contracts:compileJava` +- Compile the wire/version leaf when you touched moved message/schema/address/version/probe code: + - `./gradlew :interop-wire:compileJava` - Compile the config/l10n leaf when you touched extracted config or l10n sources: - `./gradlew :foundation-config:classes` - Compile only the runtime SPI leaf when you touched just that JDK-only API surface: diff --git a/.agents/skills/cryptad-crypto-aead/SKILL.md b/.agents/skills/cryptad-crypto-aead/SKILL.md index 402227cd82c..86554095dde 100644 --- a/.agents/skills/cryptad-crypto-aead/SKILL.md +++ b/.agents/skills/cryptad-crypto-aead/SKILL.md @@ -13,6 +13,13 @@ Use this skill when you touch: - On-disk encryption formats for persistent files or plugin stores - Any migration/compatibility path involving OCB or AES-GCM +## Source ownership +- `network.crypta.crypt` and `network.crypta.keys` now live in the `:foundation-crypto-keys` + subproject. +- Canonical source root: `foundation-crypto-keys/src/main/java/` +- The root project and other leaf projects depend on `:foundation-crypto-keys`; do not recreate + duplicate crypto helpers in root-owned code when a reusable leaf-owned type already exists. + ## Current state (breaking change) - AEAD has migrated from OCB to AES-GCM (breaking). - On-disk prefix remains 16 bytes: @@ -26,8 +33,8 @@ Use this skill when you touch: - Plugin stores (`*.data.crypt`) cannot be read; plugins start with empty/default store data. ### Primary files -- `src/main/java/network/crypta/crypt/AEADInputStream.java` -- `src/main/java/network/crypta/crypt/AEADOutputStream.java` +- `foundation-crypto-keys/src/main/java/network/crypta/crypt/AEADInputStream.java` +- `foundation-crypto-keys/src/main/java/network/crypta/crypt/AEADOutputStream.java` ## Legacy note: OCB nonce compatibility (do not regress) Historically: diff --git a/.agents/skills/cryptad-packaging/SKILL.md b/.agents/skills/cryptad-packaging/SKILL.md index a65bacba908..14f427c8791 100644 --- a/.agents/skills/cryptad-packaging/SKILL.md +++ b/.agents/skills/cryptad-packaging/SKILL.md @@ -20,12 +20,17 @@ Use this skill when working on: - Packaging remains root-owned. - The root project `:cryptad` still owns `buildJar`, `assembleCryptadDist`, `dist*`, `run`, `runLauncher`, and jpackage tasks. -- Current contributing leaf modules are `:foundation-support`, `:foundation-store-contracts`, +- Current contributing leaf modules are `:foundation-support`, `:foundation-store`, + `:foundation-store-contracts`, `:foundation-crypto-keys`, `:interop-wire`, `:foundation-config`, `:foundation-fs`, `:foundation-compat`, `:runtime-spi`, `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. - Extracted leaf modules contribute jars and resources through the root runtime classpath. - `:foundation-support` and `:foundation-store-contracts` contribute shared runtime classes via their leaf JARs like the other extracted modules. +- `:foundation-crypto-keys` and `:foundation-store` contribute the extracted crypto/key/store + runtime classes through their leaf JARs. +- `:interop-wire` contributes the extracted message/schema/version/probe nucleus and serializer + classes through its leaf JAR. - `:foundation-config` contributes the config/l10n code and main l10n resources via its leaf JAR and re-exports `:foundation-support` and `:foundation-fs` where public APIs expose those types. - The `:runtime-spi` JAR is packaged like the other leaf artifacts; packaging still produces one diff --git a/README.md b/README.md index 2c3b4f3ab82..da71b979918 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,21 @@ Cryptad now uses a partial multi-project Gradle build. `run`, `runLauncher`, `assembleCryptadDist`, and jpackage task graph. - `:foundation-support` owns the current stable generic support subset under `network.crypta.support`, `network.crypta.support.api`, `network.crypta.support.io`, - `network.crypta.support.compress`, `network.crypta.support.math`, plus - `network.crypta.node.FSParseException`. + `network.crypta.support.compress`, `network.crypta.support.math`, + `network.crypta.support.transport.ip`, plus `network.crypta.io.AddressIdentifier`, + `network.crypta.io.WritableToDataOutputStream`, `network.crypta.node.FSParseException`, + `network.crypta.node.FastRunnable`, and `network.crypta.node.SemiOrderedShutdownHook`. - `:foundation-store-contracts` owns the neutral `network.crypta.store` contracts - `BlockMetadata`, `GetPubkey`, and `StorableBlock`. + `BlockMetadata`, `GetPubkey`, and `StorableBlock`, plus the store-maintenance alert seam under + `network.crypta.store.alerts`. +- `:foundation-crypto-keys` owns `network.crypta.crypt`, `network.crypta.keys`, and the + crypto-adjacent `network.crypta.support.io.BucketTools` and + `network.crypta.support.io.PrependLengthOutputStream` helpers. +- `:foundation-store` owns the reusable `network.crypta.store` implementations plus + `network.crypta.store.caching` and `network.crypta.store.saltedhash`. +- `:interop-wire` owns the narrow wire/message/schema/version/probe nucleus: leaf-safe + `network.crypta.io.comm` message/schema classes, `network.crypta.node.Version`, + `network.crypta.node.probe.Error` and `Type`, and `network.crypta.support.Serializer`. - `:foundation-config` owns `network.crypta.config`, `network.crypta.l10n`, and the main `network/crypta/l10n/crypta.l10n.en.properties` resource. Its public APIs now export `:foundation-support` and `:foundation-fs` where config types expose `SimpleFieldSet` or @@ -188,11 +199,13 @@ Cryptad now uses a partial multi-project Gradle build. - `:launcher-desktop` owns `network.crypta.launcher`, `com.jthemedetecor`, `oshi`, and launcher resources. - The large cyclic daemon core remains in the root project for now, and all tests still live - there. Most store implementations and daemon-coupled support code remain root-owned. + there. The root still owns daemon-coupled transport/socket code in `network.crypta.io.comm`, + runtime adapters and helpers under `network.crypta.node.runtime`, and the remaining + daemon-coupled support/UI wiring. - Higher-level infrastructure now crosses a narrower boundary through - `network.crypta.runtime.spi.RuntimePorts`, implemented in the root project by - `network.crypta.node.runtime.LegacyRuntimePorts`, plus HTTP-local wiring records and - package-local seams such as + `network.crypta.runtime.spi.RuntimePorts`, the minimal wire-side `MessageSource` seam used by + leaf-owned messages, and package-local seams such as + `network.crypta.node.runtime.LegacyRuntimePorts`, `network.crypta.clients.http.BookmarkEditorToadletRuntimePorts`, `network.crypta.clients.http.ConfigToadletRuntimePorts`, `network.crypta.clients.http.ConnectionsToadletRuntimePorts`, @@ -468,10 +481,18 @@ cd build/jpackage/Crypta.app/Contents artifacts; use the commands in “Spotless + Dependency Verification” below. Root build also includes: -- `:foundation-support`: extracted stable support/api/io/compress/math subset plus - `network.crypta.node.FSParseException`. -- `:foundation-store-contracts`: neutral store contracts used by `crypt`, `keys`, and `store` - code during extraction work. +- `:foundation-support`: extracted stable support/api/io/compress/math/transport subset plus + `network.crypta.io.AddressIdentifier`, `network.crypta.io.WritableToDataOutputStream`, + `network.crypta.node.FSParseException`, `network.crypta.node.FastRunnable`, and + `network.crypta.node.SemiOrderedShutdownHook`. +- `:foundation-store-contracts`: neutral store contracts plus the store-maintenance alert seam + shared by store code and root runtime/UI adapters. +- `:foundation-crypto-keys`: extracted `network.crypta.crypt`, `network.crypta.keys`, and the + adjacent `BucketTools` / `PrependLengthOutputStream` helpers. +- `:foundation-store`: extracted reusable `network.crypta.store` implementations, caching, and + salted-hash storage code. +- `:interop-wire`: extracted wire/message/schema/address/version/probe nucleus plus + `network.crypta.support.Serializer`. - `:foundation-config`: extracted config/l10n code and main l10n resources. Its public APIs re-export `:foundation-support` and `:foundation-fs` where required. - `:launcher-desktop`: Swing launcher code and desktop/theme detection dependencies. @@ -526,15 +547,26 @@ Tip: Keep the Spotless formatter at the intended version (currently `googleJavaF - Root project `:cryptad` remains the daemon/application build and still owns the strongly coupled core packages, all tests, packaging/runtime tasks, and the current `LegacyRuntimePorts` bridge into the runtime SPI. - - Leaf subprojects are `:foundation-support`, `:foundation-store-contracts`, + - Leaf subprojects are `:foundation-support`, `:foundation-store`, + `:foundation-store-contracts`, `:foundation-crypto-keys`, `:interop-wire`, `:foundation-config`, `:foundation-fs`, `:foundation-compat`, `:runtime-spi`, `:thirdparty-onion`, `:thirdparty-legacy`, and `:launcher-desktop`. - Core network (`network.crypta.node`): `Node`, `PeerNode`, `PeerManager`, `PacketSender`, `RequestStarter`, `RequestScheduler`, `NodeUpdateManager`. - Storage (`network.crypta.store`): `FreenetStore`, `CHKStore`, `SSKStore`, `SlashdotStore`. - Neutral contracts such as `BlockMetadata`, `GetPubkey`, and `StorableBlock` now live in - `:foundation-store-contracts`; store implementations still remain in the root project. -- Crypto (`network.crypta.crypt`): AES, DSA/ECDSA, SHA‑256, `RandomSource`/Yarrow. -- Keys (`network.crypta.keys`): `ClientCHK`, `ClientSSK`, `FreenetURI`, USK. + `:foundation-store` now owns the reusable store implementations, cache layer, and salted-hash + storage code. `:foundation-store-contracts` owns the neutral contracts plus the + `network.crypta.store.alerts` seam used by root runtime/UI adapters such as + `UserAlertManagerStoreAlertSink`. +- Crypto (`network.crypta.crypt`): AES, DSA/ECDSA, SHA‑256, `RandomSource`/Yarrow. This package + now lives in `:foundation-crypto-keys`. +- Keys (`network.crypta.keys`): `ClientCHK`, `ClientSSK`, `FreenetURI`, USK. This package now + lives in `:foundation-crypto-keys`. +- Wire/message nucleus (`network.crypta.io.comm`, `network.crypta.node.Version`, + `network.crypta.node.probe`, `network.crypta.support.Serializer`): `:interop-wire` owns the + leaf-safe message/schema/address/version/probe subset, including `Message`, `MessageType`, + `Peer`, `FreenetInetAddress`, `Version`, and the probe enums. The root project still owns the + transport/socket/filter side of `network.crypta.io.comm`, and `Message` now depends on the + minimal `MessageSource` seam rather than directly on `PeerContext`. - Clients: `network.crypta.client`, FCP (`network.crypta.clients.fcp`), HTTP (`network.crypta.clients.http`). FCP now consumes execution, randomness, transfer policy, lifecycle, config access, and detached peer mutations through `RuntimePorts` and FCP-local @@ -567,17 +599,22 @@ Tip: Keep the Spotless formatter at the intended version (currently `googleJavaF filesystem-facing types. Higher layers should still prefer `RuntimePorts#config()` and the root `LegacyConfigPort` bridge instead of reaching through daemon internals. - Support foundation leaf (`:foundation-support`): stable generic support, support-api, - support-io, support-compress, and support-math classes plus - `network.crypta.node.FSParseException`. + support-io, support-compress, support-math, and transport-IP classes plus + `network.crypta.io.AddressIdentifier`, `network.crypta.io.WritableToDataOutputStream`, + `network.crypta.node.FSParseException`, `network.crypta.node.FastRunnable`, and + `network.crypta.node.SemiOrderedShutdownHook`. - Support (`network.crypta.support`): logging, data structures, threading, and helpers are now split between `:foundation-support` and the root project. Keep generic reusable utilities in the foundation leaf; daemon-coupled support code still remains in root. - Launcher/Desktop: `:launcher-desktop` provides `network.crypta.launcher`, `com.jthemedetecor`, launcher resources, and desktop-theme integration. - Extracted foundations: `:foundation-support` provides the stable generic support subset, - `:foundation-store-contracts` provides neutral `network.crypta.store` contracts, - `:foundation-config` provides config/l10n, `:foundation-fs` provides `network.crypta.fs`, and - `:foundation-compat` provides `network.crypta.compat`. + `:foundation-store-contracts` provides neutral store contracts and alert seams, + `:foundation-crypto-keys` provides `network.crypta.crypt` and `network.crypta.keys`, + `:foundation-store` provides reusable store implementations, `:interop-wire` provides the + wire/version/probe nucleus, `:foundation-config` provides config/l10n, + `:foundation-fs` provides `network.crypta.fs`, and `:foundation-compat` provides + `network.crypta.compat`. - Runtime boundary leaf: `:runtime-spi` provides `network.crypta.runtime.spi`. - Vendored libraries: `:thirdparty-onion` provides `com.onionnetworks`, `:thirdparty-legacy` provides `org.bitpedia`, `org.sevenzip`, and `org.spaceroots`. diff --git a/build.gradle.kts b/build.gradle.kts index 4d61f85907b..45f77e0ff68 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,7 @@ val internalLeafProjects = project(":foundation-store"), project(":foundation-store-contracts"), project(":foundation-crypto-keys"), + project(":interop-wire"), project(":foundation-config"), project(":foundation-fs"), project(":foundation-compat"), @@ -41,6 +42,7 @@ dependencies { implementation(project(":foundation-store")) implementation(project(":foundation-store-contracts")) implementation(project(":foundation-crypto-keys")) + implementation(project(":interop-wire")) implementation(project(":foundation-config")) implementation(project(":foundation-fs")) implementation(project(":foundation-compat")) diff --git a/interop-wire/build.gradle.kts b/interop-wire/build.gradle.kts new file mode 100644 index 00000000000..20e8e889ee6 --- /dev/null +++ b/interop-wire/build.gradle.kts @@ -0,0 +1,66 @@ +import java.io.IOException +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.compile.JavaCompile + +plugins { + id("cryptad.java-kotlin-conventions") + id("cryptad.spotless") + `java-library` +} + +version = rootProject.version + +dependencies { + api(project(":foundation-support")) + api(project(":foundation-crypto-keys")) + + implementation(libs.slf4jApi) + + compileOnly(libs.jetbrainsAnnotations) +} + +val versionBuildDir = file("$projectDir/build/tmp/compileVersion/") +val versionSrc = "network/crypta/node/Version.java" + +val gitrev: String = + try { + val cmd = "git rev-parse --short HEAD" + ProcessBuilder(cmd.split(" ")) + .directory(rootDir) + .start() + .inputStream + .bufferedReader() + .readText() + .trim() + } catch (_: IOException) { + "@unknown@" + } + +val sourceSetsContainer: SourceSetContainer = extensions.getByType(SourceSetContainer::class.java) + +val generateVersionSource by + tasks.registering(Copy::class) { + val buildVersion = project.version.toString() + val javaSrcDirs = sourceSetsContainer["main"].java.srcDirs + val templateInputs = javaSrcDirs.map { it.resolve(versionSrc) } + inputs.files(templateInputs) + inputs.property("buildVersion", buildVersion) + inputs.property("gitRevision", gitrev) + outputs.file(file(versionBuildDir.resolve(versionSrc))) + + from(javaSrcDirs) { + include(versionSrc) + filter { line: String -> + line.replace("@build_number@", buildVersion).replace("@git_rev@", gitrev) + } + } + into(versionBuildDir) + } + +tasks.named("compileJava") { + dependsOn(generateVersionSource) + source(versionBuildDir) + inputs.property("buildNumber", project.version.toString()) + inputs.property("gitRevision", gitrev) + inputs.files(generateVersionSource) +} diff --git a/interop-wire/gradle/owned-output-patterns.txt b/interop-wire/gradle/owned-output-patterns.txt new file mode 100644 index 00000000000..f1a685f0308 --- /dev/null +++ b/interop-wire/gradle/owned-output-patterns.txt @@ -0,0 +1,33 @@ +# Aggregated main outputs that must be pruned on non-clean builds because :interop-wire owns them now. +# Keep this list limited to the narrow message/schema/version/probe nucleus extracted in PR-75. + +network/crypta/io/comm/AsyncMessageCallback* +network/crypta/io/comm/ByteCounter* +network/crypta/io/comm/DMT* +network/crypta/io/comm/Dispatcher* +network/crypta/io/comm/DisconnectedException* +network/crypta/io/comm/DuplicateMessageTypeException* +network/crypta/io/comm/FreenetInetAddress* +network/crypta/io/comm/IncorrectTypeException* +network/crypta/io/comm/Message.class +network/crypta/io/comm/Message$*.class +network/crypta/io/comm/MessageSource.class +network/crypta/io/comm/MessageSource$*.class +network/crypta/io/comm/MessageType.class +network/crypta/io/comm/MessageType$*.class +network/crypta/io/comm/NotConnectedException* +network/crypta/io/comm/OpennetAnnounceRequest* +network/crypta/io/comm/Peer.class +network/crypta/io/comm/Peer$*.class +network/crypta/io/comm/PeerParseException* +network/crypta/io/comm/PeerRestartedException* +network/crypta/io/comm/ReferenceSignatureVerificationException* +network/crypta/io/comm/RetrievalException* +network/crypta/io/comm/TrafficClass* +network/crypta/node/Version.class +network/crypta/node/Version$*.class +network/crypta/node/VersionParseException* +network/crypta/node/probe/Error* +network/crypta/node/probe/Type* +network/crypta/support/Serializer.class +network/crypta/support/Serializer$*.class diff --git a/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java b/interop-wire/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java similarity index 100% rename from src/main/java/network/crypta/io/comm/AsyncMessageCallback.java rename to interop-wire/src/main/java/network/crypta/io/comm/AsyncMessageCallback.java diff --git a/src/main/java/network/crypta/io/comm/ByteCounter.java b/interop-wire/src/main/java/network/crypta/io/comm/ByteCounter.java similarity index 100% rename from src/main/java/network/crypta/io/comm/ByteCounter.java rename to interop-wire/src/main/java/network/crypta/io/comm/ByteCounter.java diff --git a/src/main/java/network/crypta/io/comm/DMT.java b/interop-wire/src/main/java/network/crypta/io/comm/DMT.java similarity index 100% rename from src/main/java/network/crypta/io/comm/DMT.java rename to interop-wire/src/main/java/network/crypta/io/comm/DMT.java diff --git a/src/main/java/network/crypta/io/comm/DisconnectedException.java b/interop-wire/src/main/java/network/crypta/io/comm/DisconnectedException.java similarity index 100% rename from src/main/java/network/crypta/io/comm/DisconnectedException.java rename to interop-wire/src/main/java/network/crypta/io/comm/DisconnectedException.java diff --git a/src/main/java/network/crypta/io/comm/Dispatcher.java b/interop-wire/src/main/java/network/crypta/io/comm/Dispatcher.java similarity index 100% rename from src/main/java/network/crypta/io/comm/Dispatcher.java rename to interop-wire/src/main/java/network/crypta/io/comm/Dispatcher.java diff --git a/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java b/interop-wire/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java similarity index 100% rename from src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java rename to interop-wire/src/main/java/network/crypta/io/comm/DuplicateMessageTypeException.java diff --git a/src/main/java/network/crypta/io/comm/FreenetInetAddress.java b/interop-wire/src/main/java/network/crypta/io/comm/FreenetInetAddress.java similarity index 100% rename from src/main/java/network/crypta/io/comm/FreenetInetAddress.java rename to interop-wire/src/main/java/network/crypta/io/comm/FreenetInetAddress.java diff --git a/src/main/java/network/crypta/io/comm/IncorrectTypeException.java b/interop-wire/src/main/java/network/crypta/io/comm/IncorrectTypeException.java similarity index 100% rename from src/main/java/network/crypta/io/comm/IncorrectTypeException.java rename to interop-wire/src/main/java/network/crypta/io/comm/IncorrectTypeException.java diff --git a/src/main/java/network/crypta/io/comm/Message.java b/interop-wire/src/main/java/network/crypta/io/comm/Message.java similarity index 97% rename from src/main/java/network/crypta/io/comm/Message.java rename to interop-wire/src/main/java/network/crypta/io/comm/Message.java index 7fe4045e1a6..9694f061696 100644 --- a/src/main/java/network/crypta/io/comm/Message.java +++ b/interop-wire/src/main/java/network/crypta/io/comm/Message.java @@ -42,7 +42,7 @@ public class Message { "$Id: Message.java,v 1.11 2005/09/15 18:16:04 amphibian Exp $"; private final MessageType spec; - private final WeakReference sourceRef; + private final WeakReference sourceRef; private final boolean internal; private final HashMap payload = HashMap.newHashMap(8); private List subMessages; @@ -74,7 +74,7 @@ public class Message { * @return a decoded {@code Message}, or {@code null} if decoding fails or the type is rejected */ public static Message decodeMessageFromPacket( - byte[] buf, int offset, int length, PeerContext peer, int overhead) { + byte[] buf, int offset, int length, MessageSource peer, int overhead) { ByteBufferInputStream bb = new ByteBufferInputStream(buf, offset, length); return decodeMessage(bb, peer, length + overhead, true, false, false); } @@ -82,7 +82,7 @@ public static Message decodeMessageFromPacket( /** * Decodes a message using relaxed validation. * - *

Compared to {@link #decodeMessageFromPacket(byte[], int, int, PeerContext, int)}, this + *

Compared to {@link #decodeMessageFromPacket(byte[], int, int, MessageSource, int)}, this * variant accepts certain legacy or malformed inputs that the strict decoder would reject. * * @param buf array containing the entire payload @@ -90,14 +90,14 @@ public static Message decodeMessageFromPacket( * @param overhead additional bytes attributed to the received packet for statistics * @return a decoded {@code Message}, or {@code null} when decoding fails */ - public static Message decodeMessageLax(byte[] buf, PeerContext peer, int overhead) { + public static Message decodeMessageLax(byte[] buf, MessageSource peer, int overhead) { ByteBufferInputStream bb = new ByteBufferInputStream(buf); return decodeMessage(bb, peer, buf.length + overhead, true, false, true); } private static Message decodeMessage( ByteBufferInputStream bb, - PeerContext peer, + MessageSource peer, int recvByteCount, boolean mayHaveSubMessages, boolean inSubMessage, @@ -150,7 +150,7 @@ private static boolean isAcceptable(MessageType mspec) { private static void readAndAttachSubMessagesIfAny( Message m, ByteBufferInputStream bb, - PeerContext peer, + MessageSource peer, boolean veryLax, boolean mayHaveSubMessages) { if (!mayHaveSubMessages) return; @@ -158,7 +158,7 @@ private static void readAndAttachSubMessagesIfAny( } private static void logPrematureEnd( - PeerContext peer, MessageType mspec, boolean inSubMessage, EOFException e) { + MessageSource peer, MessageType mspec, boolean inSubMessage, EOFException e) { String msg = peer.getPeer() + " sent a message packet that ends prematurely while deserialising " @@ -192,7 +192,7 @@ private static void readMessageFields(MessageType mspec, Message m, ByteBufferIn } private static void readAndAttachSubMessages( - Message m, ByteBufferInputStream bb, PeerContext peer, boolean veryLax) { + Message m, ByteBufferInputStream bb, MessageSource peer, boolean veryLax) { while (bb.remaining() > 2) { // sizeof(unsigned short) == 2 ByteBufferInputStream bb2 = readSubMessageSlice(bb, m); if (bb2 == null) { @@ -223,7 +223,7 @@ private static ByteBufferInputStream readSubMessageSlice(ByteBufferInputStream b } private static Message decodeSubMessage( - ByteBufferInputStream bb2, PeerContext peer, boolean veryLax) { + ByteBufferInputStream bb2, MessageSource peer, boolean veryLax) { try { return decodeMessage(bb2, peer, 0, false, true, veryLax); } catch (Exception e) { @@ -241,7 +241,7 @@ public Message(MessageType spec) { this(spec, null, 0); } - private Message(MessageType spec, PeerContext source, int recvByteCount) { + private Message(MessageType spec, MessageSource source, int recvByteCount) { localInstantiationTime = System.currentTimeMillis(); this.spec = spec; if (source == null) { @@ -586,9 +586,9 @@ public String toString() { *

For locally constructed messages this is {@code null}. For decoded messages a weak reference * is held to the peer context and may have been cleared by the GC. * - * @return {@code null} for local messages, or a possibly {@code null} {@link PeerContext} + * @return {@code null} for local messages, or a possibly {@code null} {@link MessageSource} */ - public PeerContext getSource() { + public MessageSource getSource() { return sourceRef == null ? null : sourceRef.get(); } diff --git a/interop-wire/src/main/java/network/crypta/io/comm/MessageSource.java b/interop-wire/src/main/java/network/crypta/io/comm/MessageSource.java new file mode 100644 index 00000000000..a12a0b32809 --- /dev/null +++ b/interop-wire/src/main/java/network/crypta/io/comm/MessageSource.java @@ -0,0 +1,11 @@ +package network.crypta.io.comm; + +import java.lang.ref.WeakReference; + +public interface MessageSource { + Peer getPeer(); + + long getBootID(); + + WeakReference getWeakRef(); +} diff --git a/src/main/java/network/crypta/io/comm/MessageType.java b/interop-wire/src/main/java/network/crypta/io/comm/MessageType.java similarity index 100% rename from src/main/java/network/crypta/io/comm/MessageType.java rename to interop-wire/src/main/java/network/crypta/io/comm/MessageType.java diff --git a/src/main/java/network/crypta/io/comm/NotConnectedException.java b/interop-wire/src/main/java/network/crypta/io/comm/NotConnectedException.java similarity index 100% rename from src/main/java/network/crypta/io/comm/NotConnectedException.java rename to interop-wire/src/main/java/network/crypta/io/comm/NotConnectedException.java diff --git a/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java b/interop-wire/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java similarity index 100% rename from src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java rename to interop-wire/src/main/java/network/crypta/io/comm/OpennetAnnounceRequest.java diff --git a/src/main/java/network/crypta/io/comm/Peer.java b/interop-wire/src/main/java/network/crypta/io/comm/Peer.java similarity index 100% rename from src/main/java/network/crypta/io/comm/Peer.java rename to interop-wire/src/main/java/network/crypta/io/comm/Peer.java diff --git a/src/main/java/network/crypta/io/comm/PeerParseException.java b/interop-wire/src/main/java/network/crypta/io/comm/PeerParseException.java similarity index 100% rename from src/main/java/network/crypta/io/comm/PeerParseException.java rename to interop-wire/src/main/java/network/crypta/io/comm/PeerParseException.java diff --git a/src/main/java/network/crypta/io/comm/PeerRestartedException.java b/interop-wire/src/main/java/network/crypta/io/comm/PeerRestartedException.java similarity index 100% rename from src/main/java/network/crypta/io/comm/PeerRestartedException.java rename to interop-wire/src/main/java/network/crypta/io/comm/PeerRestartedException.java diff --git a/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java b/interop-wire/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java similarity index 100% rename from src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java rename to interop-wire/src/main/java/network/crypta/io/comm/ReferenceSignatureVerificationException.java diff --git a/src/main/java/network/crypta/io/comm/RetrievalException.java b/interop-wire/src/main/java/network/crypta/io/comm/RetrievalException.java similarity index 100% rename from src/main/java/network/crypta/io/comm/RetrievalException.java rename to interop-wire/src/main/java/network/crypta/io/comm/RetrievalException.java diff --git a/src/main/java/network/crypta/io/comm/TrafficClass.java b/interop-wire/src/main/java/network/crypta/io/comm/TrafficClass.java similarity index 100% rename from src/main/java/network/crypta/io/comm/TrafficClass.java rename to interop-wire/src/main/java/network/crypta/io/comm/TrafficClass.java diff --git a/src/main/java/network/crypta/node/Version.java b/interop-wire/src/main/java/network/crypta/node/Version.java similarity index 100% rename from src/main/java/network/crypta/node/Version.java rename to interop-wire/src/main/java/network/crypta/node/Version.java diff --git a/src/main/java/network/crypta/node/VersionParseException.java b/interop-wire/src/main/java/network/crypta/node/VersionParseException.java similarity index 100% rename from src/main/java/network/crypta/node/VersionParseException.java rename to interop-wire/src/main/java/network/crypta/node/VersionParseException.java diff --git a/src/main/java/network/crypta/node/probe/Error.java b/interop-wire/src/main/java/network/crypta/node/probe/Error.java similarity index 100% rename from src/main/java/network/crypta/node/probe/Error.java rename to interop-wire/src/main/java/network/crypta/node/probe/Error.java diff --git a/src/main/java/network/crypta/node/probe/Type.java b/interop-wire/src/main/java/network/crypta/node/probe/Type.java similarity index 100% rename from src/main/java/network/crypta/node/probe/Type.java rename to interop-wire/src/main/java/network/crypta/node/probe/Type.java diff --git a/src/main/java/network/crypta/support/Serializer.java b/interop-wire/src/main/java/network/crypta/support/Serializer.java similarity index 100% rename from src/main/java/network/crypta/support/Serializer.java rename to interop-wire/src/main/java/network/crypta/support/Serializer.java diff --git a/settings.gradle.kts b/settings.gradle.kts index c7dd834c58b..c9fcf4385ca 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,7 @@ include( ":foundation-store", ":foundation-store-contracts", ":foundation-crypto-keys", + ":interop-wire", ":foundation-config", ":foundation-fs", ":foundation-compat", diff --git a/src/main/java/network/crypta/io/comm/PeerContext.java b/src/main/java/network/crypta/io/comm/PeerContext.java index e297abf539e..ec39f8df162 100644 --- a/src/main/java/network/crypta/io/comm/PeerContext.java +++ b/src/main/java/network/crypta/io/comm/PeerContext.java @@ -19,25 +19,14 @@ * * @author amphibian */ -public interface PeerContext { +public interface PeerContext extends MessageSource { // Largely opaque interface for now. - /** - * Returns the transport identity for this peer. - * - *

The value may be {@code null} if the peer identity is unknown, transient, or has been - * cleared (for example, after disconnect or GC of backing state). Callers should not retain or - * mutate the returned object. - * - * @return the peer descriptor, or {@code null} when unavailable. - */ - Peer getPeer(); - /** * Requests that the underlying connection be closed. * - *

Pending operations may fail or be cancelled; subsequent calls to {@link #isConnected()} - * generally return {@code false}. + *

Pending operations may fail or be canceled; later calls to {@link #isConnected()} generally + * return {@code false}. */ void forceDisconnect(); @@ -82,9 +71,9 @@ public interface PeerContext { * cancel the sending. If provided, {@link AsyncMessageCallback} is invoked as delivery * progresses. Implementations should account bytes via {@link ByteCounter} where appropriate. * - * @param msg the message to send; must be constructed for this send path (not reused from a + * @param msg the message to send; must be constructed for this sending path (not reused from a * different source). - * @param cb optional callback invoked on send events; may be {@code null}. + * @param cb optional callback invoked on sending events; may be {@code null}. * @param ctr byte counter to update for statistics and throttling; may be {@code null} if the * caller does not wish to record counts. * @return a handle representing the queued message. @@ -95,14 +84,6 @@ default MessageItem sendAsync(Message msg, AsyncMessageCallback cb, ByteCounter return transport().sendAsync(msg, cb, ctr); } - /** - * Returns the current boot identifier for the remote peer. - * - *

The value changes on each restart of the remote node and can be used to detect restarts - * while in-flight operations are pending. - */ - long getBootID(); - /** * Returns the packet-level throttle for the peer's current address. * @@ -116,6 +97,7 @@ default PacketThrottle getThrottle() { } /** Returns the transport handler that receives packets from this peer. */ + @SuppressWarnings("unused") default SocketHandler getSocketHandler() { return transport().getSocketHandler(); } @@ -129,6 +111,7 @@ default SocketHandler getSocketHandler() { *

Implementations are encouraged to reuse a single weak reference per instance to avoid * allocation overhead. */ + @Override WeakReference getWeakRef(); /** Returns a compact, log-friendly description of this peer. */ @@ -151,7 +134,7 @@ default SocketHandler getSocketHandler() { boolean unqueueMessage(MessageItem item); /** - * Reports the time spent waiting for a send slot due to throttling. + * Reports the time spent waiting for a sending slot due to throttling. * * @param time elapsed time in milliseconds. * @param realTime {@code true} for the real-time path; {@code false} for bulk. diff --git a/src/main/java/network/crypta/node/NodeDataRequestHandler.java b/src/main/java/network/crypta/node/NodeDataRequestHandler.java index 61a2e859486..b0865eb8c84 100644 --- a/src/main/java/network/crypta/node/NodeDataRequestHandler.java +++ b/src/main/java/network/crypta/node/NodeDataRequestHandler.java @@ -6,6 +6,7 @@ import network.crypta.io.comm.Message; import network.crypta.io.comm.MessageType; import network.crypta.io.comm.NotConnectedException; +import network.crypta.io.comm.PeerContext; import network.crypta.keys.Key; import network.crypta.keys.KeyBlock; import network.crypta.keys.NodeSSK; @@ -326,8 +327,9 @@ private void handleDataRequest(Message m, boolean isSSK) { private void rejectRequest(Message m, ByteCounter ctr) { long uid = m.getLong(DMT.UID); Message msg = DMT.createFNPRejectedOverload(uid, true); + PeerContext source = (PeerContext) m.getSource(); try { - m.getSource().transport().sendAsync(msg, null, ctr); + source.transport().sendAsync(msg, null, ctr); } catch (NotConnectedException _) { // Ignore } diff --git a/src/main/java/network/crypta/node/NodeNotRoutableHandler.java b/src/main/java/network/crypta/node/NodeNotRoutableHandler.java index ec0fdab6fd5..1340a964229 100644 --- a/src/main/java/network/crypta/node/NodeNotRoutableHandler.java +++ b/src/main/java/network/crypta/node/NodeNotRoutableHandler.java @@ -6,6 +6,7 @@ import network.crypta.io.comm.Message; import network.crypta.io.comm.MessageType; import network.crypta.io.comm.NotConnectedException; +import network.crypta.io.comm.PeerContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,8 +20,8 @@ * rejected quickly and consistently. * *

The handler is intentionally stateless aside from a reference to the owning {@link Node}. It - * does not maintain per-peer state, cache results, or schedule asynchronous work beyond the - * transport send itself. Callers should treat it as a pure decision step: if it returns {@code + * does not maintain a per-peer state, cache results, or schedule asynchronous work beyond the + * transport sending itself. Callers should treat it as a pure decision step: if it returns {@code * true}, a response has been queued; if it returns {@code false}, the caller should continue with * normal processing. No internal synchronization is performed, so callers are expected to invoke it * on threads that already coordinate node message handling. @@ -38,7 +39,7 @@ final class NodeNotRoutableHandler { /** Logger used for trace-level diagnostics during early rejection decisions. */ private static final Logger LOG = LoggerFactory.getLogger(NodeNotRoutableHandler.class); - /** Owning node instance used to access routing state and network statistics. */ + /** The owning node instance used to access routing state and network statistics. */ private final Node node; /** @@ -59,8 +60,8 @@ final class NodeNotRoutableHandler { * *

This method inspects the message type and, for known request types that cannot be routed * from the source, sends a rejection response that mirrors the request UID. The response is - * queued through the transport layer and accounted against the appropriate byte counter. When the - * message type is not handled by this class, the method returns {@code false} and performs no + * queued through the transport layer and accounted against the appropriate byte counter. When + * this class does not handle the message type, the method returns {@code false} and performs no * side effects. * *

The method is idempotent with respect to its return value but not with respect to network @@ -96,8 +97,8 @@ boolean handle(Message m) { * Sends a rejection response for the given message using the supplied counter. * *

This method extracts the request UID, builds a rejection response, and queues it on the - * transport. If the peer disconnects before the send is queued, the exception is ignored so that - * upstream dispatch logic can continue without additional error handling. + * transport. If the peer disconnects before the sending is queued, the exception is ignored so + * that upstream dispatch logic can continue without additional error handling. * * @param m message whose UID is mirrored in the rejection response; must be non-null. * @param ctr byte counter used for accounting the reject response; must be non-null. @@ -105,8 +106,9 @@ boolean handle(Message m) { private void rejectRequest(Message m, ByteCounter ctr) { long uid = m.getLong(DMT.UID); Message msg = DMT.createFNPRejectedOverload(uid, true); + PeerContext source = (PeerContext) m.getSource(); try { - m.getSource().transport().sendAsync(msg, null, ctr); + source.transport().sendAsync(msg, null, ctr); } catch (NotConnectedException _) { // Ignore }