From 1ef57f32aff876c65d92db3a0caaaa7f86bbac4d Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 23 Jan 2024 13:14:18 -0500 Subject: [PATCH] Stub in `TorRuntime` APIs (#329) --- gradle/libs.versions.toml | 9 +- .../runtime-ctrl-api/api/runtime-ctrl-api.api | 5 + .../kmp/tor/runtime/ctrl/api/Blocks.kt | 79 +++- .../kmp/tor/runtime/ctrl/api/TorEvent.kt | 20 +- .../tor/runtime/ctrl/api/address/IPAddress.kt | 3 + .../runtime/ctrl/api/address/OnionAddress.kt | 2 + .../kmp/tor/runtime/ctrl/api/address/Port.kt | 2 + library/runtime-ctrl/api/runtime-ctrl.api | 7 + .../runtime/ctrl/AbstractTorEventProcessor.kt | 82 +++- .../ctrl/AbstractTorEventProcessorUnitTest.kt | 85 +++- library/runtime-mobile/.gitignore | 1 + library/runtime-mobile/api/runtime-mobile.api | 7 + library/runtime-mobile/build.gradle.kts | 77 ++++ library/runtime-mobile/gradle.properties | 3 + .../runtime/mobile/AndroidTorRuntimeTest.kt | 43 ++ .../mobile/OwningControllerProcessTest.kt | 34 ++ .../mobile/TorRuntimeEnvironmentTest.kt | 67 +++ .../mobile/TorServiceInitializerTest.kt | 33 ++ .../src/androidMain/AndroidManifest.xml | 38 ++ .../tor/runtime/mobile/AbstractTorService.kt} | 17 +- .../runtime/mobile/TorRuntimeEnvironment.kt | 86 ++++ .../kmp/tor/runtime/mobile/TorService.kt | 122 ++++++ .../mobile/AndroidTorRuntimeUnitTest.kt | 40 ++ .../mobile/TorServiceInitializerUnitTest.kt | 33 ++ library/runtime/api/runtime.api | 135 ++++++ library/runtime/build.gradle.kts | 10 + .../kmp/tor/runtime/NetworkObserver.kt | 121 ++++++ .../kmp/tor/runtime/RuntimeEvent.kt | 203 +++++++++ .../kmp/tor/runtime/TorRuntime.kt | 399 ++++++++++++++++++ .../tor/runtime/internal/-CommonPlatform.kt | 30 ++ .../internal/AbstractRuntimeEventProcessor.kt | 144 +++++++ .../tor/runtime/internal/InstanceKeeper.kt | 37 ++ .../tor/runtime/internal/RealTorRuntime.kt | 159 +++++++ .../AbstractRuntimeEventProcessorUnitTest.kt | 145 +++++++ .../tor/runtime/NetworkObserverUnitTest.kt | 81 ++++ .../runtime/TorRuntimeEnvironmentUnitTest.kt | 43 ++ .../kmp/tor/runtime/internal/-JvmPlatform.kt | 56 +++ .../tor/runtime/internal/-NonJvmPlatform.kt | 37 ++ settings.gradle.kts | 1 + 39 files changed, 2447 insertions(+), 49 deletions(-) create mode 100644 library/runtime-mobile/.gitignore create mode 100644 library/runtime-mobile/api/runtime-mobile.api create mode 100644 library/runtime-mobile/build.gradle.kts create mode 100644 library/runtime-mobile/gradle.properties create mode 100644 library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AndroidTorRuntimeTest.kt create mode 100644 library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/OwningControllerProcessTest.kt create mode 100644 library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorRuntimeEnvironmentTest.kt create mode 100644 library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorServiceInitializerTest.kt create mode 100644 library/runtime-mobile/src/androidMain/AndroidManifest.xml rename library/{runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/-Stub.kt => runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AbstractTorService.kt} (58%) create mode 100644 library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorRuntimeEnvironment.kt create mode 100644 library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorService.kt create mode 100644 library/runtime-mobile/src/androidUnitTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AndroidTorRuntimeUnitTest.kt create mode 100644 library/runtime-mobile/src/androidUnitTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorServiceInitializerUnitTest.kt create mode 100644 library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/NetworkObserver.kt create mode 100644 library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/RuntimeEvent.kt create mode 100644 library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorRuntime.kt create mode 100644 library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-CommonPlatform.kt create mode 100644 library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/AbstractRuntimeEventProcessor.kt create mode 100644 library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/InstanceKeeper.kt create mode 100644 library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/RealTorRuntime.kt create mode 100644 library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/AbstractRuntimeEventProcessorUnitTest.kt create mode 100644 library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/NetworkObserverUnitTest.kt create mode 100644 library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorRuntimeEnvironmentUnitTest.kt create mode 100644 library/runtime/src/jvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-JvmPlatform.kt create mode 100644 library/runtime/src/nonJvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-NonJvmPlatform.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 013f2fa5d..bd8c2317b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,5 @@ [versions] +androidx-startup = "1.1.1" androidx-test-core = "1.5.0" androidx-test-runner = "1.5.2" @@ -10,12 +11,13 @@ gradle-kmp-configuration = "0.1.7" gradle-kotlin = "1.9.21" gradle-publish-maven = "0.25.3" -kmp-tor-core = "2.0.0-alpha05" +kmp-tor-core = "2.0.0-alpha06" kmp-tor-resource = "408.10.0-SNAPSHOT" - +kotlincrypto-hash = "0.4.0" kotlinx-coroutines = "1.7.3" [libraries] +androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } @@ -29,8 +31,9 @@ gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version. gradle-publish-maven = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "gradle-publish-maven" } kmp-tor-core-api = { module = "io.matthewnelson.kmp-tor:core-api", version.ref = "kmp-tor-core" } +kmp-tor-core-lib-locator = { module = "io.matthewnelson.kmp-tor:core-lib-locator", version.ref = "kmp-tor-core" } kmp-tor-core-resource = { module = "io.matthewnelson.kmp-tor:core-resource", version.ref = "kmp-tor-core" } - +kotlincrypto-hash-sha2 = { module = "org.kotlincrypto.hash:sha2", version.ref = "kotlincrypto-hash" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-javafx = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-javafx", version.ref = "kotlinx-coroutines" } diff --git a/library/runtime-ctrl-api/api/runtime-ctrl-api.api b/library/runtime-ctrl-api/api/runtime-ctrl-api.api index 4a3de39bf..8732f707e 100644 --- a/library/runtime-ctrl-api/api/runtime-ctrl-api.api +++ b/library/runtime-ctrl-api/api/runtime-ctrl-api.api @@ -1,5 +1,6 @@ public final class io/matthewnelson/kmp/tor/runtime/ctrl/api/BlocksKt { public static final fun apply (Ljava/lang/Object;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ItBlock;)Ljava/lang/Object; + public static final fun apply (Ljava/lang/Object;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock$WithIt;Ljava/lang/Object;)Ljava/lang/Object; public static final fun apply (Ljava/lang/Object;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock;)Ljava/lang/Object; } @@ -16,6 +17,10 @@ public abstract interface class io/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBl public abstract fun invoke (Ljava/lang/Object;)V } +public abstract interface class io/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock$WithIt { + public abstract fun invoke (Ljava/lang/Object;Ljava/lang/Object;)V +} + public final class io/matthewnelson/kmp/tor/runtime/ctrl/api/TorConfig { public static final field Companion Lio/matthewnelson/kmp/tor/runtime/ctrl/api/TorConfig$Companion; public final field settings Ljava/util/Set; diff --git a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/Blocks.kt b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/Blocks.kt index 9f98b37aa..a78d79de8 100644 --- a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/Blocks.kt +++ b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/Blocks.kt @@ -19,52 +19,85 @@ package io.matthewnelson.kmp.tor.runtime.ctrl.api /** * Helper for non-Kotlin consumers instead of using - * `T.() -> Unit` which would force a return of `Unit`. * - * e.g. + * T.() -> Unit + * + * which would force a return of `Unit`. + * + * e.g. (Kotlin) + * + * My.Builder { + * add(My.Factory) { enable = true } + * } + * + * e.g. (Java) * - * // Java * My.Builder(b -> { * b.add(My.Factory.Companion, s -> { * s.enable = true; * }); * }); * - * // Kotlin - * My.Builder { - * add(My.Factory) { - * enable = true - * } - * } - * * @see [ItBlock] + * @see [WithIt] * @see [apply] * */ public fun interface ThisBlock { public operator fun T.invoke() + + /** + * Helper for non-Kotlin consumers instead of using + * + * T.(V) -> Unit + * + * which would force a return of `Unit`. + * + * e.g. (Kotlin) + * + * My.Builder { + * add(My.Factory) { arg -> enable = arg.someLogic() } + * } + * + * e.g. (Java) + * + * My.Builder(b -> { + * b.add(My.Factory.Companion, (s, arg) -> { + * s.enable = arg.someLogic(); + * }); + * }); + * + * @see [ThisBlock] + * @see [ItBlock] + * @see [apply] + * */ + public fun interface WithIt { + public operator fun T.invoke(it: V) + } } /** * Helper for non-Kotlin consumers instead of using - * `(T) -> Unit` which would force a return of `Unit`. * - * e.g. + * (T) -> Unit + * + * which would force a return of `Unit`. + * + * e.g. (Kotlin) + * + * My.Builder { + * it.add(My.Factory) { s -> s.enable = true } + * } + * + * e.g. (Java) * - * // Java * My.Builder(b -> { * b.add(My.Factory.Companion, s -> { * s.enable = true; * }); * }); * - * // Kotlin - * My.Builder { - * it.add(My.Factory) { s -> - * s.enable = true - * } - * } - * * @see [ThisBlock] + * @see [ThisBlock.WithIt] * @see [apply] * */ public fun interface ItBlock { @@ -77,6 +110,12 @@ public inline fun T.apply(block: ThisBlock): T { return this } +@Suppress("NOTHING_TO_INLINE") +public inline fun T.apply(block: ThisBlock.WithIt, arg: V): T { + with(block) { invoke(arg) } + return this +} + @Suppress("NOTHING_TO_INLINE") public inline fun T.apply(block: ItBlock): T { block(this) diff --git a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent.kt b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent.kt index 957b76233..931639105 100644 --- a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent.kt +++ b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent.kt @@ -189,17 +189,18 @@ public enum class TorEvent { /** * Create an observer for the given [TorEvent] + * to register via [Processor.add] * * e.g. (Kotlin) * - * val bwObserver = TorEvent.BW.observer { event -> - * updateNotification(event.formatBandwidth()) + * TorEvent.BW.observer { output -> + * updateNotification(output.formatBandwidth()) * } * * e.g. (Java) * - * TorEvent.Observer bwObserver = TorEvent.BW.observer(e -> { - * updateNotification(formatBandwidth(e)); + * TorEvent.BW.observer(output -> { + * updateNotification(formatBandwidth(output)); * }); * * @param [block] the callback to pass the event text to @@ -209,7 +210,8 @@ public enum class TorEvent { ): Observer = observer("", block) /** - * Create an observer for the given [TorEvent] and [tag]. + * Create an observer for the given [TorEvent] and [tag] + * to register via [Processor.add] * * This is useful for lifecycle aware components, all of which * can be removed with a single call using the [tag] upon @@ -217,14 +219,14 @@ public enum class TorEvent { * * e.g. (Kotlin) * - * val bwObserver = TorEvent.BW.observer("my service") { event -> - * updateNotification(event.formatBandwidth()) + * TorEvent.BW.observer("my service") { output -> + * updateNotification(output.formatBandwidth()) * } * * e.g. (Java) * - * TorEvent.Observer bwObserver = TorEvent.BW.observer("my service", e -> { - * updateNotification(formatBandwidth(e)); + * TorEvent.BW.observer("my service", output -> { + * updateNotification(formatBandwidth(output)); * }); * * @param [tag] Any non-blank string value diff --git a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/IPAddress.kt b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/IPAddress.kt index 730004c60..ac9d4f029 100644 --- a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/IPAddress.kt +++ b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/IPAddress.kt @@ -23,6 +23,9 @@ import kotlin.jvm.JvmStatic /** * Base abstraction for denoting a String value as an ip address + * + * @see [V4] + * @see [V6] * */ public sealed class IPAddress private constructor(value: String): Address(value) { diff --git a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/OnionAddress.kt b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/OnionAddress.kt index 548dc18ee..c51e36230 100644 --- a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/OnionAddress.kt +++ b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/OnionAddress.kt @@ -29,6 +29,8 @@ import kotlin.jvm.JvmStatic /** * Base abstraction for denoting a String value as a `.onion` address + * + * @see [V3] * */ public sealed class OnionAddress private constructor(value: String): Address(value) { diff --git a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/Port.kt b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/Port.kt index 545767737..02d7e5c58 100644 --- a/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/Port.kt +++ b/library/runtime-ctrl-api/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/api/address/Port.kt @@ -23,6 +23,8 @@ import kotlin.jvm.JvmStatic /** * Holder for a port between 0 and 65535 (inclusive) + * + * @see [Proxy] * */ public open class Port private constructor( @JvmField diff --git a/library/runtime-ctrl/api/runtime-ctrl.api b/library/runtime-ctrl/api/runtime-ctrl.api index a12faa946..920e08d32 100644 --- a/library/runtime-ctrl/api/runtime-ctrl.api +++ b/library/runtime-ctrl/api/runtime-ctrl.api @@ -1,8 +1,12 @@ public abstract class io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessor : io/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent$Processor { + protected static final field Companion Lio/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessor$Companion; public final fun add (Lio/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent$Observer;)V public final fun add ([Lio/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent$Observer;)V public fun clearObservers ()V + protected final fun isDestroyed ()Z + protected final fun isStaticTag (Ljava/lang/String;)Z protected final fun notifyObservers (Lio/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent;Ljava/lang/String;)V + protected fun onDestroy ()V public final fun remove (Lio/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent$Observer;)V public final fun remove ([Lio/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent$Observer;)V public final fun removeAll (Lio/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent;)V @@ -11,3 +15,6 @@ public abstract class io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProc protected final fun withObservers (Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; } +protected final class io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessor$Companion { +} + diff --git a/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessor.kt b/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessor.kt index d713fd4ea..2a8fcc150 100644 --- a/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessor.kt +++ b/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessor.kt @@ -19,6 +19,10 @@ import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi import io.matthewnelson.kmp.tor.core.resource.SynchronizedObject import io.matthewnelson.kmp.tor.core.resource.synchronized import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorEvent +import kotlin.concurrent.Volatile +import kotlin.jvm.JvmField +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic /** * Base abstraction for implementations that process [TorEvent]. @@ -26,9 +30,15 @@ import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorEvent public abstract class AbstractTorEventProcessor @InternalKmpTorApi protected constructor( + private val staticTag: String?, initialObservers: Set ): TorEvent.Processor { + @Volatile + @get:JvmName("isDestroyed") + protected var isDestroyed: Boolean = false + private set + private val observers = initialObservers.toMutableSet() @OptIn(InternalKmpTorApi::class) @@ -57,6 +67,8 @@ protected constructor( val iterator = iterator() while (iterator.hasNext()) { val observer = iterator.next() + if (observer.tag.isStaticTag()) continue + if (observer.event == event) { iterator.remove() } @@ -70,6 +82,8 @@ protected constructor( val iterator = iterator() while (iterator.hasNext()) { val observer = iterator.next() + if (observer.tag.isStaticTag()) continue + if (events.contains(observer.event)) { iterator.remove() } @@ -78,10 +92,14 @@ protected constructor( } public override fun removeAll(tag: String) { + if (tag.isStaticTag()) return + withObservers { val iterator = iterator() while (iterator.hasNext()) { val observer = iterator.next() + if (observer.tag.isStaticTag()) continue + if (observer.tag == tag) { iterator.remove() } @@ -90,10 +108,18 @@ protected constructor( } public override fun clearObservers() { - withObservers { clear() } + withObservers { + val iterator = iterator() + while (iterator.hasNext()) { + val observer = iterator.next() + if (observer.tag.isStaticTag()) continue + iterator.remove() + } + } } - protected fun notifyObservers(event: TorEvent, output: String) { + protected fun TorEvent.notifyObservers(output: String) { + val event = this withObservers { for (observer in this) { if (observer.event != event) continue @@ -102,14 +128,58 @@ protected constructor( } } + protected open fun onDestroy() { + if (isDestroyed) return + withObservers { clear(); isDestroyed = true } + } + + @OptIn(InternalKmpTorApi::class) protected fun withObservers( block: MutableSet.() -> T, ): T { - @OptIn(InternalKmpTorApi::class) - val result = synchronized(lock) { - block(observers) + if (isDestroyed) return block(noOpMutableSet()) + + return synchronized(lock) { + block(if (isDestroyed) noOpMutableSet() else observers) } + } + + protected fun String?.isStaticTag(): Boolean = this != null && staticTag != null && this == staticTag + + protected companion object { + + @JvmStatic + @InternalKmpTorApi + @Suppress("UNCHECKED_CAST") + protected fun noOpMutableSet(): MutableSet = NoOpMutableSet as MutableSet + } +} + +private object NoOpMutableSet: MutableSet { + + override fun equals(other: Any?): Boolean = other is MutableSet<*> && other.isEmpty() + override fun hashCode(): Int = 0 + override fun toString(): String = "[]" + + override val size: Int get() = 0 + override fun isEmpty(): Boolean = true + override fun contains(element: Any): Boolean = false + override fun containsAll(elements: Collection): Boolean = elements.isEmpty() + + override fun iterator(): MutableIterator = EmptyMutableIterator + + override fun add(element: Any): Boolean = false + override fun addAll(elements: Collection): Boolean = elements.isEmpty() + + override fun clear() {} + + override fun retainAll(elements: Collection): Boolean = elements.isEmpty() + override fun removeAll(elements: Collection): Boolean = elements.isEmpty() + override fun remove(element: Any): Boolean = false - return result + private object EmptyMutableIterator: MutableIterator { + override fun hasNext(): Boolean = false + override fun next(): Any = throw NoSuchElementException() + override fun remove() { throw IllegalStateException() } } } diff --git a/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessorUnitTest.kt b/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessorUnitTest.kt index 3f6d45d08..1180305da 100644 --- a/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessorUnitTest.kt +++ b/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/AbstractTorEventProcessorUnitTest.kt @@ -17,16 +17,16 @@ package io.matthewnelson.kmp.tor.runtime.ctrl import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorEvent -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull +import kotlin.test.* @OptIn(InternalKmpTorApi::class) class AbstractTorEventProcessorUnitTest { - private class TestProcessor: AbstractTorEventProcessor(emptySet()) { + private class TestProcessor: AbstractTorEventProcessor("static", emptySet()) { val size: Int get() = withObservers { size } - fun notify(event: TorEvent, output: String) { notifyObservers(event, output) } + fun notify(event: TorEvent, output: String) { event.notifyObservers(output) } + fun destroy() { onDestroy() } + fun noOpSet(): MutableSet = noOpMutableSet() } private val processor = TestProcessor() @@ -43,7 +43,7 @@ class AbstractTorEventProcessorUnitTest { } @Test - fun givenObservers_whenRemoveByEvent_thenAreRemoved() { + fun givenObservers_whenRemoveAllByEvent_thenAreRemoved() { var invocations = 0 val o1 = TorEvent.CIRC.observer { invocations++ } val o2 = TorEvent.BW.observer {} @@ -59,7 +59,7 @@ class AbstractTorEventProcessorUnitTest { } @Test - fun givenObservers_whenRemoveAll_thenAreRemoved() { + fun givenObservers_whenRemoveMultiple_thenAreRemoved() { var invocations = 0 val o1 = TorEvent.CIRC.observer { invocations++ } val o2 = TorEvent.BW.observer {} @@ -95,4 +95,75 @@ class AbstractTorEventProcessorUnitTest { fun givenBlankTag_whenObserver_thenTagIsNull() { assertNull(TorEvent.Observer(" ", TorEvent.CIRC) { }.tag) } + + @Test + fun givenStaticTag_whenRemove_thenDoesNothing() { + processor.add(TorEvent.BW.observer("static") {}) + + val nonStaticObserver = TorEvent.BW.observer("non-static") {} + processor.add(nonStaticObserver) + + // should do nothing + processor.removeAll("static") + assertEquals(2, processor.size) + + // Should only remove the non-static observer + processor.removeAll(TorEvent.BW) + assertEquals(1, processor.size) + + // Should only remove the non-static observer + processor.add(nonStaticObserver) + assertEquals(2, processor.size) + processor.removeAll(TorEvent.BW, TorEvent.ADDRMAP) + assertEquals(1, processor.size) + + // Should not remove the static observer + processor.add(nonStaticObserver) + assertEquals(2, processor.size) + processor.clearObservers() + assertEquals(1, processor.size) + } + + @Test + fun givenStaticObservers_whenOnDestroy_thenEvictsAll() { + val observer = TorEvent.BW.observer("static") {} + processor.add(observer) + assertEquals(1, processor.size) + + processor.clearObservers() + assertEquals(1, processor.size) + + processor.destroy() + assertEquals(0, processor.size) + + processor.add(observer) + assertEquals(0, processor.size) + } + + @Test + fun givenNoOpMutableSet_whenModified_thenDoesNothing() { + val set = processor.noOpSet() + assertEquals(0, set.size) + assertEquals(0, set.apply { add("") }.size) + assertFalse(set.add("")) + assertTrue(set.addAll(emptyList())) + assertFalse(set.addAll(listOf("", "a"))) + assertTrue(set.retainAll(emptySet())) + assertFalse(set.retainAll(listOf("", "a"))) + assertTrue(set.removeAll(emptySet())) + assertFalse(set.removeAll(listOf("", "a"))) + assertFalse(set.remove("a")) + assertTrue(set.isEmpty()) + assertFalse(set.contains("")) + assertTrue(set.containsAll(emptySet())) + assertFalse(set.containsAll(listOf("", "a"))) + + // does nothing + set.clear() + + val iterator = set.iterator() + assertFalse(iterator.hasNext()) + assertFailsWith { iterator.next() } + assertFailsWith { iterator.remove() } + } } diff --git a/library/runtime-mobile/.gitignore b/library/runtime-mobile/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/library/runtime-mobile/.gitignore @@ -0,0 +1 @@ +/build diff --git a/library/runtime-mobile/api/runtime-mobile.api b/library/runtime-mobile/api/runtime-mobile.api new file mode 100644 index 000000000..66ff00dd5 --- /dev/null +++ b/library/runtime-mobile/api/runtime-mobile.api @@ -0,0 +1,7 @@ +public final class io/matthewnelson/kmp/tor/runtime/mobile/TorRuntimeEnvironment { + public static final fun Builder (Landroid/content/Context;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment; + public static final fun Builder (Landroid/content/Context;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment; + public static final fun Builder (Landroid/content/Context;Lkotlin/jvm/functions/Function1;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment; + public static final fun Builder (Landroid/content/Context;Lkotlin/jvm/functions/Function1;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment; +} + diff --git a/library/runtime-mobile/build.gradle.kts b/library/runtime-mobile/build.gradle.kts new file mode 100644 index 000000000..b32a8c5ed --- /dev/null +++ b/library/runtime-mobile/build.gradle.kts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2021 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +plugins { + id("configuration") +} + +kmpConfiguration { + configure { + androidLibrary(namespace = "io.matthewnelson.kmp.tor.runtime.mobile") { + target { publishLibraryVariants("release") } + + android { + lint { + // linter does not like the subclass. Runtime tests + // are performed to ensure everything is copacetic. + disable.add("EnsureInitializerMetadata") + } + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + } + + sourceSetMain { + dependencies { + implementation(libs.androidx.startup.runtime) + implementation(libs.kotlinx.coroutines.android) + } + } + + sourceSetTest { + dependencies { + implementation(libs.kmp.tor.core.lib.locator) + } + } + + sourceSetTestInstrumented { + dependencies { + implementation(libs.androidx.test.core) + implementation(libs.androidx.test.runner) + } + } + } + + iosAll() + + common { + pluginIds("publication") + + sourceSetMain { + dependencies { + api(project(":library:runtime")) + } + } + sourceSetTest { + dependencies { + implementation(kotlin("test")) + } + } + } + + kotlin { explicitApi() } + } +} diff --git a/library/runtime-mobile/gradle.properties b/library/runtime-mobile/gradle.properties new file mode 100644 index 000000000..4bf0d025a --- /dev/null +++ b/library/runtime-mobile/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=runtime-mobile +POM_NAME=KmpTor Runtime For Mobile +POM_DESCRIPTION=Mobile specific runtime utilities diff --git a/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AndroidTorRuntimeTest.kt b/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AndroidTorRuntimeTest.kt new file mode 100644 index 000000000..237f62d58 --- /dev/null +++ b/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AndroidTorRuntimeTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.mobile + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller.Paths +import io.matthewnelson.kmp.tor.runtime.TorRuntime +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class AndroidTorRuntimeTest { + + private val app = ApplicationProvider.getApplicationContext() + + @Test + fun givenTorRuntime_whenAndroidRuntime_thenIsAndroidTorRuntime() { + val environment = app.createTorRuntimeEnvironment { installationDir -> + object : ResourceInstaller(installationDir) { + override fun install(): Paths.Tor { fail() } + } + } + + val runtime = TorRuntime.Builder(environment) {} + + assertEquals("AndroidTorRuntime", runtime::class.simpleName) + } +} diff --git a/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/OwningControllerProcessTest.kt b/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/OwningControllerProcessTest.kt new file mode 100644 index 000000000..85ca53ddc --- /dev/null +++ b/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/OwningControllerProcessTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.mobile + +import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorConfig +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class OwningControllerProcessTest { + + @Test + fun givenSetting_whenDefault_thenIsAsExpected() { + val setting = TorConfig.__OwningControllerProcess.Builder { + // Default PID should match as it's using reflection + assertEquals(android.os.Process.myPid(), processId) + } + assertNotNull(setting) + println(setting) + } +} diff --git a/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorRuntimeEnvironmentTest.kt b/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorRuntimeEnvironmentTest.kt new file mode 100644 index 000000000..3ddaf6c4b --- /dev/null +++ b/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorRuntimeEnvironmentTest.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.mobile + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class TorRuntimeEnvironmentTest { + + private val app = ApplicationProvider.getApplicationContext() + + @Test + fun givenContext_whenDefaultDirname_thenIsAsExpected() { + val environment = app.createTorRuntimeEnvironment { installationDir -> + object : ResourceInstaller(installationDir) { + override fun install(): Paths.Tor { fail() } + } + } + + assertEquals("app_torservice", environment.workDir.name) + assertEquals("torservice", environment.cacheDir.name) + } + + @Test + fun givenContext_whenDefaultDirnameWithConfigurationBlock_thenIsAsExpected() { + val environment = app.createTorRuntimeEnvironment( + installer = { installationDir -> + object : ResourceInstaller(installationDir) { + override fun install(): Paths.Tor { fail() } + } + }, + block = {}, + ) + + assertEquals("app_torservice", environment.workDir.name) + assertEquals("torservice", environment.cacheDir.name) + } + + @Test + fun givenContext_whenBlankDirName_thenIsAsExpected() { + val environment = app.createTorRuntimeEnvironment(dirName = " ") { installationDir -> + object : ResourceInstaller(installationDir) { + override fun install(): Paths.Tor { fail() } + } + } + + assertEquals("app_torservice", environment.workDir.name) + assertEquals("torservice", environment.cacheDir.name) + } +} diff --git a/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorServiceInitializerTest.kt b/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorServiceInitializerTest.kt new file mode 100644 index 000000000..3937f3c1c --- /dev/null +++ b/library/runtime-mobile/src/androidInstrumentedTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorServiceInitializerTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.mobile + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class TorServiceInitializerTest { + + @Test + fun givenAndroidEmulator_whenIsInitialized_thenIsTrue() { + assertTrue(TorService.Initializer.isInitialized()) + } + + @Test + fun givenDependencies_whenKmpTorLibLocatorNotPresent_thenDoesNotAddAsDependency() { + assertEquals(0, TorService.Initializer().dependencies().size) + } +} diff --git a/library/runtime-mobile/src/androidMain/AndroidManifest.xml b/library/runtime-mobile/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..952f2dd7c --- /dev/null +++ b/library/runtime-mobile/src/androidMain/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/-Stub.kt b/library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AbstractTorService.kt similarity index 58% rename from library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/-Stub.kt rename to library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AbstractTorService.kt index a3d74c228..72bae2d23 100644 --- a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/-Stub.kt +++ b/library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AbstractTorService.kt @@ -1,11 +1,11 @@ /* - * Copyright (c) 2023 Matthew Nelson + * Copyright (c) 2024 Matthew Nelson * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,6 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -package io.matthewnelson.kmp.tor.runtime +package io.matthewnelson.kmp.tor.runtime.mobile -internal fun stub() { /* no-op */ } +import android.app.Service +import android.content.Intent +import android.os.IBinder + +internal sealed class AbstractTorService: Service() { + override fun onBind(intent: Intent?): IBinder? { + // TODO + return null + } +} diff --git a/library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorRuntimeEnvironment.kt b/library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorRuntimeEnvironment.kt new file mode 100644 index 000000000..4ef691b3a --- /dev/null +++ b/library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorRuntimeEnvironment.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +@file:JvmName("TorRuntimeEnvironment") + +package io.matthewnelson.kmp.tor.runtime.mobile + +import android.content.Context +import io.matthewnelson.kmp.file.File +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller.Paths +import io.matthewnelson.kmp.tor.runtime.TorRuntime +import io.matthewnelson.kmp.tor.runtime.ctrl.api.ThisBlock + +/** + * Android extension which utilizes [Context.getDir] and [Context.getCacheDir] + * to acquire default tor directory locations within the application's data + * directory to create [TorRuntime.Environment]. + * + * workDir: app_torservice + * cacheDir: cache/torservice + * */ +@JvmName("Builder") +public fun Context.createTorRuntimeEnvironment( + installer: (installationDir: File) -> ResourceInstaller, +): TorRuntime.Environment = createTorRuntimeEnvironment("torservice", installer) + +/** + * Android extension which utilizes [Context.getDir] and [Context.getCacheDir] + * to acquire default tor directory locations within the application's data + * directory to create [TorRuntime.Environment]. + * + * workDir: app_torservice + * cacheDir: cache/torservice + * */ +@JvmName("Builder") +public fun Context.createTorRuntimeEnvironment( + installer: (installationDir: File) -> ResourceInstaller, + block: ThisBlock, +): TorRuntime.Environment = createTorRuntimeEnvironment("torservice", installer, block) + +/** + * Android extension which utilizes [Context.getDir] and [Context.getCacheDir] + * to acquire tor directory locations within the application's data directory + * for specified [dirName] to create [TorRuntime.Environment] with. + * + * workDir: app_[dirName] + * cacheDir: cache/[dirName] + * */ +@JvmName("Builder") +public fun Context.createTorRuntimeEnvironment( + dirName: String, + installer: (installationDir: File) -> ResourceInstaller, +): TorRuntime.Environment = createTorRuntimeEnvironment(dirName, installer) {} + +/** + * Android extension which utilizes [Context.getDir] and [Context.getCacheDir] + * to acquire tor directory locations within the application's data directory + * for specified [dirName] to create [TorRuntime.Environment] with. + * + * workDir: app_[dirName] + * cacheDir: cache/[dirName] + * */ +@JvmName("Builder") +public fun Context.createTorRuntimeEnvironment( + dirName: String, + installer: (installationDir: File) -> ResourceInstaller, + block: ThisBlock, +): TorRuntime.Environment = TorRuntime.Environment.Builder( + workDir = getDir(dirName.ifBlank { "torservice" }, Context.MODE_PRIVATE), + cacheDir = cacheDir.resolve(dirName.ifBlank { "torservice" }), + installer = installer, + block = block +) diff --git a/library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorService.kt b/library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorService.kt new file mode 100644 index 000000000..bc127d905 --- /dev/null +++ b/library/runtime-mobile/src/androidMain/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorService.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.mobile + +import android.content.Context +import android.content.Intent +import androidx.startup.AppInitializer +import androidx.startup.Initializer +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.runtime.RuntimeEvent +import io.matthewnelson.kmp.tor.runtime.TorRuntime +import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorEvent +import kotlin.concurrent.Volatile + +internal class TorService internal constructor(): AbstractTorService() { + + @OptIn(InternalKmpTorApi::class) + private class AndroidTorRuntime private constructor( + private val factory: TorRuntime.ServiceFactory, + private val newIntent: () -> Intent, + ): TorRuntime, TorEvent.Processor by factory, RuntimeEvent.Processor by factory { + + override fun environment(): TorRuntime.Environment = factory.environment + + override fun removeAll(tag: String) { factory.removeAll(tag) } + override fun removeAll(vararg events: RuntimeEvent<*>) { factory.removeAll(*events) } + override fun clearObservers() { factory.clearObservers() } + + override fun startDaemon() { + // TODO + } + + override fun stopDaemon() { + // TODO + } + + override fun restartDaemon() { + // TODO + } + + companion object { + + @JvmStatic + @OptIn(InternalKmpTorApi::class) + @Throws(IllegalStateException::class) + fun create(factory: TorRuntime.ServiceFactory): TorRuntime { + TorRuntime.ServiceFactory.checkInstance(factory) + + val newIntent = newIntent + + check(newIntent != null) { "TorService.Initializer must be initialized" } + + return AndroidTorRuntime(factory, newIntent) + } + } + } + + internal class Initializer internal constructor(): androidx.startup.Initializer { + override fun create(context: Context): Companion { + if (isInitialized) return Companion + + val initializer = AppInitializer.getInstance(context) + check(initializer.isEagerlyInitialized(javaClass)) { + val classPath = "io.matthewnelson.kmp.tor.runtime.mobile.TorService$" + "Initializer" + + """ + TorService.Initializer cannot be initialized lazily. + Please ensure that you have: + + under InitializationProvider in your AndroidManifest.xml + """.trimIndent() + } + val appContext = context.applicationContext + newIntent = { Intent(appContext, TorService::class.java) } + + isInitialized = true + return Companion + } + + override fun dependencies(): List>> { + return try { + val clazz = Class + .forName("io.matthewnelson.kmp.tor.core.lib.locator.KmpTorLibLocator\$Initializer") + + @Suppress("UNCHECKED_CAST") + (clazz as? Class>)?.let { return listOf(it) } + + return emptyList() + } catch (_: Throwable) { + emptyList() + } + } + + internal companion object { + + @Volatile + private var isInitialized: Boolean = false + + @JvmSynthetic + internal fun isInitialized(): Boolean = isInitialized + } + } + + private companion object { + private var newIntent: (() -> Intent)? = null + } +} diff --git a/library/runtime-mobile/src/androidUnitTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AndroidTorRuntimeUnitTest.kt b/library/runtime-mobile/src/androidUnitTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AndroidTorRuntimeUnitTest.kt new file mode 100644 index 000000000..bf84d2425 --- /dev/null +++ b/library/runtime-mobile/src/androidUnitTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/AndroidTorRuntimeUnitTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.mobile + +import io.matthewnelson.kmp.file.toFile +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller.Paths +import io.matthewnelson.kmp.tor.runtime.TorRuntime +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class AndroidTorRuntimeUnitTest { + + @Test + fun givenTorRuntime_whenNotAndroidRuntime_thenIsNotAndroidTorRuntime() { + val environment = TorRuntime.Environment.Builder("".toFile(), "".toFile()) { installationDir -> + object : ResourceInstaller(installationDir) { + override fun install(): Paths.Tor { fail() } + } + } + + val runtime = TorRuntime.Builder(environment) {} + + assertEquals("RealTorRuntime", runtime::class.simpleName) + } +} diff --git a/library/runtime-mobile/src/androidUnitTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorServiceInitializerUnitTest.kt b/library/runtime-mobile/src/androidUnitTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorServiceInitializerUnitTest.kt new file mode 100644 index 000000000..e42919c64 --- /dev/null +++ b/library/runtime-mobile/src/androidUnitTest/kotlin/io/matthewnelson/kmp/tor/runtime/mobile/TorServiceInitializerUnitTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.mobile + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class TorServiceInitializerUnitTest { + + @Test + fun givenNonAndroidRuntime_whenIsInitialized_thenIsFalse() { + assertFalse(TorService.Initializer.isInitialized()) + } + + @Test + fun givenDependencies_whenKmpTorLibLocatorPresent_thenAddsAsDependency() { + assertEquals(1, TorService.Initializer().dependencies().size) + } +} diff --git a/library/runtime/api/runtime.api b/library/runtime/api/runtime.api index e69de29bb..6d34bfce7 100644 --- a/library/runtime/api/runtime.api +++ b/library/runtime/api/runtime.api @@ -0,0 +1,135 @@ +public abstract class io/matthewnelson/kmp/tor/runtime/NetworkObserver { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/NetworkObserver$Companion; + public static final field NOOP Lio/matthewnelson/kmp/tor/runtime/NetworkObserver; + public fun ()V + public abstract fun isNetworkConnected ()Z + protected final fun notify (Lio/matthewnelson/kmp/tor/runtime/NetworkObserver$Connectivity;)V + protected fun onObserversEmpty ()V + protected fun onObserversNotEmpty ()V +} + +public final class io/matthewnelson/kmp/tor/runtime/NetworkObserver$Companion { +} + +public final class io/matthewnelson/kmp/tor/runtime/NetworkObserver$Connectivity : java/lang/Enum { + public static final field Connected Lio/matthewnelson/kmp/tor/runtime/NetworkObserver$Connectivity; + public static final field Disconnected Lio/matthewnelson/kmp/tor/runtime/NetworkObserver$Connectivity; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/NetworkObserver$Connectivity; + public static fun values ()[Lio/matthewnelson/kmp/tor/runtime/NetworkObserver$Connectivity; +} + +public abstract class io/matthewnelson/kmp/tor/runtime/RuntimeEvent { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$Companion; + public static final field entries Ljava/util/Set; + public final fun name ()Ljava/lang/String; + public final fun observer (Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ItBlock;)Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$Observer; + public final fun observer (Ljava/lang/String;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ItBlock;)Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$Observer; + public static final fun valueOf (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent; + public static final fun valueOfOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent; +} + +public final class io/matthewnelson/kmp/tor/runtime/RuntimeEvent$Companion { + public final fun valueOf (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent; + public final fun valueOfOrNull (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent; +} + +public final class io/matthewnelson/kmp/tor/runtime/RuntimeEvent$DEBUG : io/matthewnelson/kmp/tor/runtime/RuntimeEvent { + public static final field INSTANCE Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$DEBUG; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/matthewnelson/kmp/tor/runtime/RuntimeEvent$ERROR : io/matthewnelson/kmp/tor/runtime/RuntimeEvent { + public static final field INSTANCE Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$ERROR; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/matthewnelson/kmp/tor/runtime/RuntimeEvent$INFO : io/matthewnelson/kmp/tor/runtime/RuntimeEvent { + public static final field INSTANCE Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$INFO; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/matthewnelson/kmp/tor/runtime/RuntimeEvent$Observer { + public final field event Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent; + public final field output Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ItBlock; + public final field tag Ljava/lang/String; + public fun (Ljava/lang/String;Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ItBlock;)V + public fun toString ()Ljava/lang/String; +} + +public abstract interface class io/matthewnelson/kmp/tor/runtime/RuntimeEvent$Processor { + public abstract fun add (Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$Observer;)V + public abstract fun add ([Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$Observer;)V + public abstract fun clearObservers ()V + public abstract fun remove (Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$Observer;)V + public abstract fun remove ([Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$Observer;)V + public abstract fun removeAll (Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent;)V + public abstract fun removeAll (Ljava/lang/String;)V + public abstract fun removeAll ([Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent;)V +} + +public final class io/matthewnelson/kmp/tor/runtime/RuntimeEvent$WARN : io/matthewnelson/kmp/tor/runtime/RuntimeEvent { + public static final field INSTANCE Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent$WARN; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class io/matthewnelson/kmp/tor/runtime/TorRuntime : io/matthewnelson/kmp/tor/runtime/RuntimeEvent$Processor, io/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent$Processor { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Companion; + public static fun Builder (Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime; + public abstract fun environment ()Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment; + public abstract fun restartDaemon ()V + public abstract fun startDaemon ()V + public abstract fun stopDaemon ()V +} + +public final class io/matthewnelson/kmp/tor/runtime/TorRuntime$Builder { + public field allowPortReassignment Z + public field eventThreadBackground Z + public field networkObserver Lio/matthewnelson/kmp/tor/runtime/NetworkObserver; + public field omitGeoIPFileSettings Z + public synthetic fun (Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun config (Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock$WithIt;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Builder; + public final fun staticEvent (Lio/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Builder; + public final fun staticObserver (Lio/matthewnelson/kmp/tor/runtime/RuntimeEvent;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ItBlock;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Builder; + public final fun staticObserver (Lio/matthewnelson/kmp/tor/runtime/ctrl/api/TorEvent;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ItBlock;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Builder; +} + +public final class io/matthewnelson/kmp/tor/runtime/TorRuntime$Companion { + public final fun Builder (Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime; +} + +public final class io/matthewnelson/kmp/tor/runtime/TorRuntime$Environment { + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment$Companion; + public final field cacheDir Ljava/io/File; + public final field torResource Lio/matthewnelson/kmp/tor/core/api/ResourceInstaller; + public final field torrcDefaultsFile Ljava/io/File; + public final field torrcFile Ljava/io/File; + public final field workDir Ljava/io/File; + public synthetic fun (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/io/File;Lio/matthewnelson/kmp/tor/core/api/ResourceInstaller;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun Builder (Ljava/io/File;Ljava/io/File;Lkotlin/jvm/functions/Function1;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment; + public static final fun Builder (Ljava/io/File;Ljava/io/File;Lkotlin/jvm/functions/Function1;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment; + public final fun id ()Ljava/lang/String; +} + +public final class io/matthewnelson/kmp/tor/runtime/TorRuntime$Environment$Builder { + public final field cacheDir Ljava/io/File; + public field installationDir Ljava/io/File; + public field torrcDefaultsFile Ljava/io/File; + public field torrcFile Ljava/io/File; + public final field workDir Ljava/io/File; + public synthetic fun (Ljava/io/File;Ljava/io/File;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class io/matthewnelson/kmp/tor/runtime/TorRuntime$Environment$Companion { + public final fun Builder (Ljava/io/File;Ljava/io/File;Lkotlin/jvm/functions/Function1;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment; + public final fun Builder (Ljava/io/File;Ljava/io/File;Lkotlin/jvm/functions/Function1;Lio/matthewnelson/kmp/tor/runtime/ctrl/api/ThisBlock;)Lio/matthewnelson/kmp/tor/runtime/TorRuntime$Environment; +} + diff --git a/library/runtime/build.gradle.kts b/library/runtime/build.gradle.kts index a6a27060c..16b1e88a9 100644 --- a/library/runtime/build.gradle.kts +++ b/library/runtime/build.gradle.kts @@ -24,7 +24,17 @@ kmpConfiguration { dependencies { api(project(":library:runtime-ctrl-api")) implementation(project(":library:runtime-ctrl")) + implementation(libs.encoding.base16) implementation(libs.kmp.tor.core.resource) + implementation(libs.kotlinx.coroutines.core) + } + } + } + + kotlin { + sourceSets.findByName("nonJvmMain")?.apply { + dependencies { + implementation(libs.kotlincrypto.hash.sha2) } } } diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/NetworkObserver.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/NetworkObserver.kt new file mode 100644 index 000000000..9796c6dda --- /dev/null +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/NetworkObserver.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime + +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.core.resource.SynchronizedObject +import io.matthewnelson.kmp.tor.core.resource.synchronized +import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorConfig +import kotlin.jvm.JvmField +import kotlin.jvm.JvmSynthetic + +/** + * A Hook for [TorRuntime] that controls [TorConfig.DisableNetwork] + * toggling when device connectivity is lost/gained. + * + * Multiple instances of [TorRuntime] can [subscribe] to a single + * [NetworkObserver]. + * + * @see [notify] + * @see [NOOP] + * @see [TorRuntime.Builder.networkObserver] + * */ +public abstract class NetworkObserver { + + @OptIn(InternalKmpTorApi::class) + private val lock = SynchronizedObject() + private val observers = mutableSetOf() + + internal fun interface Observer { + operator fun invoke(connectivity: Connectivity) + } + + @JvmSynthetic + internal open fun subscribe(observer: Observer) { + @OptIn(InternalKmpTorApi::class) + val initialAttach = synchronized(lock) { + val wasEmpty = observers.isEmpty() + observers.add(observer) && wasEmpty + } + + if (!initialAttach) return + + try { + onObserversNotEmpty() + } catch (_: Throwable) {} + } + + @JvmSynthetic + internal open fun unsubscribe(observer: Observer) { + @OptIn(InternalKmpTorApi::class) + val lastRemoved = synchronized(lock) { + observers.remove(observer) && observers.isEmpty() + } + + if (!lastRemoved) return + + try { + onObserversEmpty() + } catch (_: Throwable) {} + } + + /** + * Optional override for being notified when [observers] + * goes from: + * + * empty -> not empty + * */ + protected open fun onObserversNotEmpty() {} + + /** + * Optional override for being notified when [observers] + * goes from: + * + * not empty -> empty + * */ + protected open fun onObserversEmpty() {} + + public abstract fun isNetworkConnected(): Boolean + + /** + * Notifies all registered [observers] of a change in + * [Connectivity] + * */ + protected fun notify(connectivity: Connectivity) { + @OptIn(InternalKmpTorApi::class) + synchronized(lock) { + observers.forEach { it(connectivity) } + } + } + + public enum class Connectivity { + Connected, + Disconnected, + } + + public companion object { + + /** + * A non-operational [NetworkObserver] + * */ + @JvmField + public val NOOP: NetworkObserver = object : NetworkObserver() { + override fun subscribe(observer: Observer) {} + override fun unsubscribe(observer: Observer) {} + override fun isNetworkConnected(): Boolean = true + } + } +} diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/RuntimeEvent.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/RuntimeEvent.kt new file mode 100644 index 000000000..02f640355 --- /dev/null +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/RuntimeEvent.kt @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime + +import io.matthewnelson.immutable.collections.immutableSetOf +import io.matthewnelson.kmp.tor.runtime.ctrl.api.ItBlock +import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorEvent +import kotlin.jvm.JvmField +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic + +/** + * Events specific to [TorRuntime] + * + * @see [Observer] + * @see [observer] + * @see [Processor] + * */ +public sealed class RuntimeEvent private constructor() { + + @get:JvmName("name") + public val name: String get() = toString() + + /** + * Debug log messages + * */ + public data object DEBUG: RuntimeEvent() + + /** + * Error log messages + * */ + public data object ERROR: RuntimeEvent() + + /** + * Info log messages + * */ + public data object INFO: RuntimeEvent() + + /** + * Warn log messages + * */ + public data object WARN: RuntimeEvent() + + // TODO: Other events + + /** + * Create an observer for the given [RuntimeEvent] + * to register via [Processor.add] + * + * e.g. (Kotlin) + * + * RuntimeEvent.DEBUG.observer { output -> + * println(output) + * } + * + * e.g. (Java) + * + * RuntimeEvent.DEBUG.INSTANCE.observer(output -> { + * System.out.println(output); + * }); + * + * @param [block] the callback to pass the event output to + * */ + public fun observer( + block: ItBlock, + ): Observer = observer("", block) + + /** + * Create an observer for the given [RuntimeEvent] + * to register via [Processor.add] + * + * e.g. (Kotlin) + * + * RuntimeEvent.DEBUG.observer { output -> + * println(output) + * } + * + * e.g. (Java) + * + * RuntimeEvent.DEBUG.INSTANCE.observer(output -> { + * System.out.println(output); + * }); + * + * @param [tag] Any non-blank string value + * @param [block] the callback to pass the event output to + * */ + public fun observer( + tag: String, + block: ItBlock, + ): Observer = Observer(tag, this, block) + + public class Observer( + tag: String?, + @JvmField + public val event: RuntimeEvent, + @JvmField + public val output: ItBlock + ) { + @JvmField + public val tag: String? = tag?.ifBlank { null } + + override fun toString(): String = buildString { + append("RuntimeEvent.Observer[tag=") + append(tag.toString()) + append(",event=") + append(event.name) + append("]@") + append(hashCode()) + } + } + + /** + * Base interface for implementations that process [RuntimeEvent]. + * */ + public interface Processor { + + /** + * Add a single [Observer]. + * */ + public fun add(observer: Observer<*>) + + /** + * Add multiple [Observer]. + * */ + public fun add(vararg observers: Observer<*>) + + /** + * Remove a single [Observer]. + * */ + public fun remove(observer: Observer<*>) + + /** + * Remove multiple [Observer]. + * */ + public fun remove(vararg observers: Observer<*>) + + /** + * Remove all [Observer] of a single [RuntimeEvent] + * */ + public fun removeAll(event: RuntimeEvent<*>) + + /** + * Remove all [Observer] of multiple [RuntimeEvent] + * */ + public fun removeAll(vararg events: RuntimeEvent<*>) + + /** + * Remove all [Observer] with the given [tag]. + * + * If the implementing class extends both [Processor] + * and [TorEvent.Processor], [TorEvent.Observer] with + * the given [tag] will also be removed. + * */ + public fun removeAll(tag: String) + + /** + * Remove all [Observer] that are currently registered. + * + * If the implementing class extends both [Processor] + * and [TorEvent.Processor], all [TorEvent.Observer] + * will also be removed. + * */ + public fun clearObservers() + } + + public companion object { + + @JvmStatic + @Throws(IllegalArgumentException::class) + public fun valueOf(name: String): RuntimeEvent<*> { + return valueOfOrNull(name) + ?: throw IllegalArgumentException("Unknown RuntimeEvent of $name") + } + + @JvmStatic + public fun valueOfOrNull(name: String): RuntimeEvent<*>? { + return entries.firstOrNull { event -> + event.name.equals(name, ignoreCase = true) + } + } + + @JvmField + public val entries: Set> = immutableSetOf( + DEBUG, + ERROR, + INFO, + WARN, + ) + } +} diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorRuntime.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorRuntime.kt new file mode 100644 index 000000000..4d12116e2 --- /dev/null +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorRuntime.kt @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +@file:Suppress("FunctionName") + +package io.matthewnelson.kmp.tor.runtime + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import io.matthewnelson.immutable.collections.toImmutableList +import io.matthewnelson.immutable.collections.toImmutableSet +import io.matthewnelson.kmp.file.* +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller.Paths +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.core.api.annotation.KmpTorDsl +import io.matthewnelson.kmp.tor.runtime.TorRuntime.Companion.Builder +import io.matthewnelson.kmp.tor.runtime.TorRuntime.Environment.Builder +import io.matthewnelson.kmp.tor.runtime.TorRuntime.Environment.Companion.Builder +import io.matthewnelson.kmp.tor.runtime.ctrl.api.* +import io.matthewnelson.kmp.tor.runtime.internal.InstanceKeeper +import io.matthewnelson.kmp.tor.runtime.internal.RealTorRuntime +import io.matthewnelson.kmp.tor.runtime.internal.RealTorRuntime.Companion.checkInstance +import io.matthewnelson.kmp.tor.runtime.internal.sha256 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlin.jvm.JvmField +import kotlin.jvm.JvmName +import kotlin.jvm.JvmStatic +import kotlin.jvm.JvmSynthetic +import kotlin.random.Random + +public interface TorRuntime: TorEvent.Processor, RuntimeEvent.Processor { + + public fun environment(): Environment + + /** + * Starts the tor daemon. + * + * If tor is running, will do nothing. + * */ + public fun startDaemon() + + /** + * Stops the tor daemon. + * + * If tor is not running, will do nothing. + * */ + public fun stopDaemon() + + /** + * Stops and then starts the tor daemon. + * + * If tor is not running, will do nothing. + * */ + public fun restartDaemon() + + public companion object { + + /** + * Opener for creating a [TorRuntime] instance. + * + * If a [TorRuntime] has already been created for the provided + * [Environment], that [TorRuntime] instance will be returned. + * + * @param [environment] the operational environment for the instance + * @see [TorRuntime.Builder] + * */ + @JvmStatic + public fun Builder( + environment: Environment, + block: ThisBlock, + ): TorRuntime = Builder.build(environment, block) + } + + @KmpTorDsl + public class Builder private constructor( + private val environment: Environment + ) { + + private var config = mutableListOf>() + private val staticTorEvents = mutableSetOf(TorEvent.CONF_CHANGED, TorEvent.NOTICE) + private val staticTorEventObservers = mutableSetOf() + private val staticRuntimeEventObservers = mutableSetOf>() + + /** + * In the event that a configured TCP port is unavailable on the host + * device, tor will fail to startup. + * + * Setting this to true will result in reassignment of any unavailable + * TCP port arguments to "auto" just prior to startup in order to + * mitigate start failures. + * */ + @JvmField + public var allowPortReassignment: Boolean = true + + /** + * If true, [Paths.Tor.geoip] and [Paths.Tor.geoip6] will **not** be + * automatically to your [TorConfig]. + * */ + @JvmField + public var omitGeoIPFileSettings: Boolean = false + + /** + * This setting is ignored if utilizing the `kmp-tor-mobile` dependency + * as it has its own implementation which will be utilized. + * */ + @JvmField + public var networkObserver: NetworkObserver = NetworkObserver.NOOP + + /** + * If false, will use [Dispatchers.Main] when dispatching [RuntimeEvent] + * and [TorEvent] to registered observers. + * */ + @JvmField + public var eventThreadBackground: Boolean = true + + /** + * Configure the [TorConfig] at each startup. Multiple [block] may + * be set, each of which will be applied to the [TorConfig.Builder] + * before starting tor. + * + * [block] is always invoked from a background thread, so it is safe + * to perform IO within the lambda (e.g. writing settings that are + * not currently supported to the [Environment.torrcFile]). + * + * Any exception thrown within [block] will be propagated to the caller. + * + * **NOTE:** This can be omitted as a minimum viable configuration + * is always created using [Environment]. + * + * **NOTE:** [block] should not contain any non-singleton references + * such as Android Activity context. + * */ + @KmpTorDsl + public fun config( + block: ThisBlock.WithIt, + ): Builder { + config.add(block) + return this + } + + /** + * Add [TorEvent] that are required for your implementation. All + * configured [staticEvent] will be set at startup when the control + * connection is established via SETEVENTS. + * + * Any subsequent calls for SETEVENTS during runtime will be intercepted + * and modified to include all configured [staticEvent]. + * */ + @KmpTorDsl + public fun staticEvent( + event: TorEvent, + ): Builder { + staticTorEvents.add(event) + return this + } + + /** + * Add [TorEvent.Observer] which will never be removed from [TorRuntime]. + * Useful for logging purposes. + * */ + @KmpTorDsl + public fun staticObserver( + event: TorEvent, + block: ItBlock, + ): Builder { + val observer = event.observer(environment.staticObserverTag, block) + staticTorEventObservers.add(observer) + return this + } + + /** + * Add [RuntimeEvent.Observer] which will never be removed from [TorRuntime]. + * Useful for logging purposes. + * */ + @KmpTorDsl + public fun staticObserver( + event: RuntimeEvent, + block: ItBlock, + ): Builder { + val observer = event.observer(environment.staticObserverTag, block) + staticRuntimeEventObservers.add(observer) + return this + } + + internal companion object: InstanceKeeper() { + + @JvmSynthetic + internal fun build( + environment: Environment, + block: ThisBlock?, + ): TorRuntime = getOrCreateInstance(environment.id) { + val b = Builder(environment) + if (block != null) b.apply(block) + + RealTorRuntime.of( + environment = environment, + networkObserver = b.networkObserver, + allowPortReassignment = b.allowPortReassignment, + omitGeoIPFileSettings = b.omitGeoIPFileSettings, + eventThreadBackground = b.eventThreadBackground, + config = b.config.toImmutableList(), + staticTorEvents = b.staticTorEvents.toImmutableSet(), + staticTorEventObservers = b.staticTorEventObservers.toImmutableSet(), + staticRuntimeEventObservers = b.staticRuntimeEventObservers.toImmutableSet(), + ) + } + } + } + + /** + * The environment for which [TorRuntime] operates. + * + * Specified directories/files are utilized by [TorRuntime.Builder.config] + * to create a minimum viable [TorConfig]. + * + * @see [Companion.Builder] + * */ + public class Environment private constructor( + @JvmField + public val workDir: File, + @JvmField + public val cacheDir: File, + @JvmField + public val torrcFile: File, + @JvmField + public val torrcDefaultsFile: File, + @JvmField + public val torResource: ResourceInstaller, + ) { + + /** + * SHA-256 hash of the [workDir] path. + * */ + @get:JvmName("id") + public val id: String by lazy { workDir.path.encodeToByteArray().sha256() } + + // TODO: debug & ability for RealTorRuntime to attach + // TODO: hashPassword + + public companion object { + + /** + * Opener for creating a [Environment] instance. + * + * If an [Environment] has already been created for the provided + * [workDir], that [Environment] instance will be returned. + * + * [workDir] should be specified within your application's home + * directory (e.g. `$HOME/.my_application/torservice`) + * + * [cacheDir] should be specified within your application's cache + * directory (e.g. `$HOME/.my_application/cache/torservice`) + * + * It is advisable to keep the dirname for [workDir] and [cacheDir] + * identical (e.g. `torservice`), especially when creating multiple + * instances of [Environment]. + * + * When running multiple instances, declaring the same [cacheDir] as + * another [Environment] will result in a bad day. No checks are + * performed for this clash. + * + * @param [workDir] tor's working directory (e.g. `$HOME/.my_application/torservice`) + * @param [cacheDir] tor's cache directory (e.g. `$HOME/.my_application/cache/torservice`) + * @param [installer] lambda for creating [ResourceInstaller] using + * the default [Builder.installationDir] + * */ + @JvmStatic + public fun Builder( + workDir: File, + cacheDir: File, + installer: (installationDir: File) -> ResourceInstaller, + ): Environment = Builder.build(workDir, cacheDir, installer, null) + + /** + * Opener for creating a [Environment] instance. + * + * If an [Environment] has already been created for the provided + * [workDir], that [Environment] instance will be returned. + * + * [workDir] should be specified within your application's home + * directory (e.g. `$HOME/.my_application/torservice`) + * + * [cacheDir] should be specified within your application's cache + * directory (e.g. `$HOME/.my_application/cache/torservice`) + * + * It is advisable to keep the dirname for [workDir] and [cacheDir] + * identical (e.g. `torservice`), especially when creating multiple + * instances of [Environment]. + * + * When running multiple instances, declaring the same [cacheDir] as + * another [Environment] will result in a bad day. No checks are + * performed for this clash. + * + * @param [workDir] tor's working directory (e.g. `$HOME/.my_application/torservice`) + * @param [cacheDir] tor's cache directory (e.g. `$HOME/.my_application/cache/torservice`) + * @param [installer] lambda for creating [ResourceInstaller] using + * the default [Builder.installationDir] + * @param [block] optional lambda for modifying default parameters. + * */ + @JvmStatic + public fun Builder( + workDir: File, + cacheDir: File, + installer: (installationDir: File) -> ResourceInstaller, + block: ThisBlock, + ): Environment = Builder.build(workDir, cacheDir, installer, block) + } + + @KmpTorDsl + public class Builder private constructor( + @JvmField + public val workDir: File, + @JvmField + public val cacheDir: File, + ) { + + /** + * The directory for which **all** resources will be installed + * */ + @JvmField + public var installationDir: File = workDir + + @JvmField + public var torrcFile: File = workDir.resolve("torrc") + + @JvmField + public var torrcDefaultsFile: File = workDir.resolve("torrc-defaults") + + internal companion object: InstanceKeeper() { + + @JvmSynthetic + internal fun build( + workDir: File, + cacheDir: File, + installer: (installationDir: File) -> ResourceInstaller, + block: ThisBlock?, + ): Environment { + val absoluteWorkDir = workDir.absoluteFile.normalize() + + return getOrCreateInstance(key = absoluteWorkDir) { + val b = Builder(absoluteWorkDir, cacheDir.absoluteFile.normalize()) + if (block != null) b.apply(block) + + val torResource = installer(b.installationDir.absoluteFile.normalize()) + + Environment( + workDir = b.workDir, + cacheDir = b.cacheDir, + torrcFile = b.torrcFile.absoluteFile.normalize(), + torrcDefaultsFile = b.torrcDefaultsFile.absoluteFile.normalize(), + torResource = torResource, + ) + } + } + } + } + + @get:JvmSynthetic + internal val staticObserverTag: String by lazy { + Random.Default.nextBytes(16).encodeToString(Base16) + } + } + + @InternalKmpTorApi + public interface ServiceFactory: TorEvent.Processor, RuntimeEvent.Processor { + + public val environment: Environment + + public fun notify(event: RuntimeEvent, output: R) + + public fun create( + lifecycleHook: Job, + observer: NetworkObserver?, + ): TorRuntime + + @InternalKmpTorApi + public companion object { + + @JvmStatic + @InternalKmpTorApi + @Throws(IllegalStateException::class) + public fun checkInstance(factory: ServiceFactory) { factory.checkInstance() } + } + } +} diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-CommonPlatform.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-CommonPlatform.kt new file mode 100644 index 000000000..30bd09471 --- /dev/null +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-CommonPlatform.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +@file:Suppress("KotlinRedundantDiagnosticSuppress") + +package io.matthewnelson.kmp.tor.runtime.internal + +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.runtime.TorRuntime + +@OptIn(InternalKmpTorApi::class) +@Throws(IllegalStateException::class) +internal expect fun TorRuntime.ServiceFactory.Companion.serviceRuntimeOrNull( + block: () -> TorRuntime.ServiceFactory, +): TorRuntime? + +@Suppress("NOTHING_TO_INLINE") +internal expect inline fun ByteArray.sha256(): String diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/AbstractRuntimeEventProcessor.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/AbstractRuntimeEventProcessor.kt new file mode 100644 index 000000000..02d2ac728 --- /dev/null +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/AbstractRuntimeEventProcessor.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.internal + +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.core.resource.SynchronizedObject +import io.matthewnelson.kmp.tor.core.resource.synchronized +import io.matthewnelson.kmp.tor.runtime.RuntimeEvent +import io.matthewnelson.kmp.tor.runtime.ctrl.AbstractTorEventProcessor +import io.matthewnelson.kmp.tor.runtime.ctrl.api.ItBlock +import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorEvent + +@OptIn(InternalKmpTorApi::class) +internal abstract class AbstractRuntimeEventProcessor( + staticTag: String?, + initialObservers: Set>, + initialTorEventObservers: Set, +): AbstractTorEventProcessor(staticTag, initialTorEventObservers), + RuntimeEvent.Processor +{ + + private val observers = initialObservers.toMutableSet() + + private val lock = SynchronizedObject() + + public final override fun add(observer: RuntimeEvent.Observer<*>) { + withRuntimeObservers { add(observer) } + } + + public final override fun add(vararg observers: RuntimeEvent.Observer<*>) { + if (observers.isEmpty()) return + withRuntimeObservers { observers.forEach { add(it) } } + } + + public final override fun remove(observer: RuntimeEvent.Observer<*>) { + withRuntimeObservers { remove(observer) } + } + + public final override fun remove(vararg observers: RuntimeEvent.Observer<*>) { + if (observers.isEmpty()) return + withRuntimeObservers { observers.forEach { remove(it) } } + } + + public final override fun removeAll(event: RuntimeEvent<*>) { + withRuntimeObservers { + val iterator = iterator() + while (iterator.hasNext()) { + val observer = iterator.next() + if (observer.tag.isStaticTag()) continue + + if (observer.event == event) { + iterator.remove() + } + } + } + } + + public final override fun removeAll(vararg events: RuntimeEvent<*>) { + if (events.isEmpty()) return + withRuntimeObservers { + val iterator = iterator() + while (iterator.hasNext()) { + val observer = iterator.next() + if (observer.tag.isStaticTag()) continue + + if (events.contains(observer.event)) { + iterator.remove() + } + } + } + } + + public final override fun removeAll(tag: String) { + if (tag.isStaticTag()) return + + withRuntimeObservers { + val iterator = iterator() + while (iterator.hasNext()) { + val observer = iterator.next() + if (observer.tag.isStaticTag()) continue + + if (observer.tag == tag) { + iterator.remove() + } + } + } + + super.removeAll(tag) + } + + public final override fun clearObservers() { + withRuntimeObservers { + val iterator = iterator() + while (iterator.hasNext()) { + val observer = iterator.next() + if (observer.tag.isStaticTag()) continue + iterator.remove() + } + } + + super.clearObservers() + } + + protected fun RuntimeEvent.notifyObservers(output: R) { + val event = this + withRuntimeObservers { + for (observer in this) { + if (observer.event != event) continue + + @Suppress("UNCHECKED_CAST") + (observer.output as ItBlock).invoke(output) + } + } + } + + protected override fun onDestroy() { + if (isDestroyed) return + withRuntimeObservers { clear() } + super.onDestroy() + } + + protected fun withRuntimeObservers( + block: MutableSet>.() -> T, + ): T { + if (isDestroyed) return block(noOpMutableSet()) + + return synchronized(lock) { + block(if (isDestroyed) noOpMutableSet() else observers) + } + } +} diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/InstanceKeeper.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/InstanceKeeper.kt new file mode 100644 index 000000000..0ee223b2e --- /dev/null +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/InstanceKeeper.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.internal + +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.core.resource.SynchronizedObject +import io.matthewnelson.kmp.tor.core.resource.synchronized + +@OptIn(InternalKmpTorApi::class) +internal abstract class InstanceKeeper internal constructor( + initialCapacity: Int = 1, + loadFactor: Float = 1.0F, +): SynchronizedObject() { + + private val instances = LinkedHashMap(initialCapacity, loadFactor) + + protected fun getOrCreateInstance( + key: K, + block: () -> V, + ): V = synchronized(this@InstanceKeeper) { + instances[key] ?: block() + .also { instances[key] = it } + } +} diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/RealTorRuntime.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/RealTorRuntime.kt new file mode 100644 index 000000000..615b32226 --- /dev/null +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/RealTorRuntime.kt @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime.internal + +import io.matthewnelson.immutable.collections.toImmutableSet +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.runtime.NetworkObserver +import io.matthewnelson.kmp.tor.runtime.RuntimeEvent +import io.matthewnelson.kmp.tor.runtime.TorRuntime +import io.matthewnelson.kmp.tor.runtime.ctrl.api.ThisBlock +import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorConfig +import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorEvent +import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable +import kotlin.jvm.JvmSynthetic + +@OptIn(InternalKmpTorApi::class) +internal class RealTorRuntime private constructor( + private val environment: TorRuntime.Environment, + private val lifecycleHook: Job, + private val networkObserver: NetworkObserver, + private val staticTorEvents: Set, + staticTorEventObservers: Set, + staticRuntimeEventObservers: Set>, +): AbstractRuntimeEventProcessor(environment.staticObserverTag, staticRuntimeEventObservers, staticTorEventObservers), + TorRuntime +{ + + override fun environment(): TorRuntime.Environment = environment + + override fun startDaemon() { + // TODO("Not yet implemented") + } + + override fun stopDaemon() { + // TODO("Not yet implemented") + } + + override fun restartDaemon() { + // TODO("Not yet implemented") + } + + internal companion object { + + @JvmSynthetic + internal fun of( + environment: TorRuntime.Environment, + networkObserver: NetworkObserver, + allowPortReassignment: Boolean, + omitGeoIPFileSettings: Boolean, + eventThreadBackground: Boolean, + config: List>, + staticTorEvents: Set, + staticTorEventObservers: Set, + staticRuntimeEventObservers: Set>, + ): TorRuntime { + + val runtime = TorRuntime.ServiceFactory.serviceRuntimeOrNull { + ServiceFactory( + environment, + eventThreadBackground, + staticTorEvents, + staticTorEventObservers, + staticRuntimeEventObservers, + ) + } + + if (runtime != null) return runtime + + return RealTorRuntime( + environment, + NonCancellable, + networkObserver, + staticTorEvents, + staticTorEventObservers, + staticRuntimeEventObservers, + ) + } + + @JvmSynthetic + @Throws(IllegalStateException::class) + internal fun TorRuntime.ServiceFactory.checkInstance() { + check(this is ServiceFactory) { + "factory instance must be RealTorRuntime.ServiceFactory" + } + } + } + + private class ServiceFactory( + override val environment: TorRuntime.Environment, + private val eventThreadBackground: Boolean, + staticTorEvents: Set, + staticTorEventObservers: Set, + staticRuntimeEventObservers: Set>, + ): AbstractRuntimeEventProcessor( + environment.staticObserverTag, + staticRuntimeEventObservers, + staticTorEventObservers + ), TorRuntime.ServiceFactory { + + private val staticTorEvents = if (!staticTorEvents.contains(TorEvent.BW)) { + staticTorEvents.toMutableSet().apply { + add(TorEvent.BW) + }.toImmutableSet() + } else { + staticTorEvents + } + + private val staticTorEventObservers = buildSet { + val tag = environment.staticObserverTag + + TorEvent.entries.forEach { event -> + val observer = event.observer(tag) { event.notifyObservers(it) } + add(observer) + } + }.toImmutableSet() + + private val staticRuntimeEventObservers = buildSet { + val tag = environment.staticObserverTag + + RuntimeEvent.entries.forEach { event -> + val observer = when (event) { + is RuntimeEvent.DEBUG -> event.observer(tag) { event.notifyObservers(it) } + is RuntimeEvent.ERROR -> event.observer(tag) { event.notifyObservers(it) } + is RuntimeEvent.INFO -> event.observer(tag) { event.notifyObservers(it) } + is RuntimeEvent.WARN -> event.observer(tag) { event.notifyObservers(it) } + } + add(observer) + } + }.toImmutableSet() + + override fun notify(event: RuntimeEvent, output: R) { event.notifyObservers(output) } + + override fun create( + lifecycleHook: Job, + observer: NetworkObserver?, + ): TorRuntime = RealTorRuntime( + environment, + lifecycleHook, + observer ?: NetworkObserver.NOOP, + staticTorEvents, + staticTorEventObservers, + staticRuntimeEventObservers, + ) + } +} diff --git a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/AbstractRuntimeEventProcessorUnitTest.kt b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/AbstractRuntimeEventProcessorUnitTest.kt new file mode 100644 index 000000000..07ebb8b7f --- /dev/null +++ b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/AbstractRuntimeEventProcessorUnitTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime + +import io.matthewnelson.kmp.tor.runtime.ctrl.api.TorEvent +import io.matthewnelson.kmp.tor.runtime.internal.AbstractRuntimeEventProcessor +import kotlin.test.* + +class AbstractRuntimeEventProcessorUnitTest { + + private class TestProcessor: AbstractRuntimeEventProcessor("static", emptySet(), emptySet()) { + val sizeRuntime: Int get() = withRuntimeObservers { size } + val sizeTor: Int get() = withObservers { size } + + fun notify(event: RuntimeEvent, output: R) { event.notifyObservers(output) } + fun destroy() { onDestroy() } + } + + private val processor = TestProcessor() + + @Test + fun givenObserver_whenAddRemove_thenIsAsExpected() { + val observer = RuntimeEvent.DEBUG.observer {} + processor.add(observer) + assertEquals(1, processor.sizeRuntime) + processor.add(observer) + assertEquals(1, processor.sizeRuntime) + processor.remove(observer) + assertEquals(0, processor.sizeRuntime) + } + + @Test + fun givenObservers_whenRemoveAllByEvent_thenAreRemoved() { + var invocations = 0 + val o1 = RuntimeEvent.DEBUG.observer { invocations++ } + val o2 = RuntimeEvent.INFO.observer {} + val o3 = RuntimeEvent.INFO.observer {} + processor.add(o1, o2, o3, o3) + assertEquals(3, processor.sizeRuntime) + + processor.removeAll(RuntimeEvent.INFO) + assertEquals(1, processor.sizeRuntime) + + processor.notify(RuntimeEvent.DEBUG, "out") + assertEquals(1, invocations) + } + + @Test + fun givenObservers_whenRemoveMultiple_thenAreRemoved() { + var invocations = 0 + val o1 = RuntimeEvent.DEBUG.observer { invocations++ } + val o2 = RuntimeEvent.INFO.observer {} + val o3 = RuntimeEvent.INFO.observer {} + processor.add(o1, o2, o3) + assertEquals(3, processor.sizeRuntime) + + processor.remove(o2, o3) + assertEquals(1, processor.sizeRuntime) + + processor.notify(RuntimeEvent.DEBUG, "out") + assertEquals(1, invocations) + } + + @Test + fun givenTaggedObserver_whenRemoveByTag_thenAreRemoved() { + var invocations = 0 + val o1 = RuntimeEvent.DEBUG.observer("test1") { invocations++ } + val o2 = RuntimeEvent.INFO.observer("test2") {} + val o3 = RuntimeEvent.WARN.observer("test2") {} + processor.add(o1, o1, o2, o2, o3, o3) + assertEquals(3, processor.sizeRuntime) + + processor.removeAll("test2") + assertEquals(1, processor.sizeRuntime) + + // Is the proper tagged observer removed + processor.notify(RuntimeEvent.DEBUG, "out") + assertEquals(1, invocations) + } + + @Test + fun givenBlankTag_whenObserver_thenTagIsNull() { + assertNull(RuntimeEvent.Observer(" ", RuntimeEvent.DEBUG) { }.tag) + } + + @Test + fun givenStaticTag_whenRemove_thenDoesNothing() { + processor.add(RuntimeEvent.DEBUG.observer("static") {}) + + val nonStaticObserver = RuntimeEvent.DEBUG.observer("non-static") {} + processor.add(nonStaticObserver) + + // should do nothing + processor.removeAll("static") + assertEquals(2, processor.sizeRuntime) + + // Should only remove the non-static observer + processor.removeAll(RuntimeEvent.DEBUG) + assertEquals(1, processor.sizeRuntime) + + // Should only remove the non-static observer + processor.add(nonStaticObserver) + assertEquals(2, processor.sizeRuntime) + processor.removeAll(RuntimeEvent.DEBUG, RuntimeEvent.WARN) + assertEquals(1, processor.sizeRuntime) + + // Should not remove the static observer + processor.add(nonStaticObserver) + assertEquals(2, processor.sizeRuntime) + processor.clearObservers() + assertEquals(1, processor.sizeRuntime) + } + + @Test + fun givenStaticObservers_whenOnDestroy_thenEvictsAll() { + val observer = RuntimeEvent.DEBUG.observer("static") {} + processor.add(observer) + processor.add(TorEvent.BW.observer("static") {}) + + processor.clearObservers() + assertEquals(1, processor.sizeRuntime) + assertEquals(1, processor.sizeTor) + + // Should also clear out static TorEvent observers + processor.destroy() + assertEquals(0, processor.sizeRuntime) + assertEquals(0, processor.sizeTor) + + processor.add(observer) + assertEquals(0, processor.sizeRuntime) + } +} diff --git a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/NetworkObserverUnitTest.kt b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/NetworkObserverUnitTest.kt new file mode 100644 index 000000000..d002cc1ac --- /dev/null +++ b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/NetworkObserverUnitTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime + +import kotlin.test.Test +import kotlin.test.assertEquals +import io.matthewnelson.kmp.tor.runtime.NetworkObserver.Observer + +class NetworkObserverUnitTest { + + private class TestNetworkObserver: NetworkObserver() { + var onObserversCount = 0 + private set + + override fun isNetworkConnected(): Boolean = true + override fun onObserversNotEmpty() { onObserversCount++ } + override fun onObserversEmpty() { onObserversCount-- } + + fun notifyInvoke() { notify(Connectivity.Connected) } + } + + private val networkObserver = TestNetworkObserver() + + @Test + fun givenAddObserver_whenMultiple_thenOnObserversNotEmptyInvokedOnFirstOnly() { + assertEquals(0, networkObserver.onObserversCount) + networkObserver.subscribe { } + assertEquals(1, networkObserver.onObserversCount) + networkObserver.subscribe { } + assertEquals(1, networkObserver.onObserversCount) + } + + @Test + fun givenRemoveObserver_whenMultiple_thenOnObserversEmptyInvokedOnLastOnly() { + assertEquals(0, networkObserver.onObserversCount) + val o1 = Observer {} + val o2 = Observer {} + networkObserver.subscribe(o1) + networkObserver.subscribe(o2) + assertEquals(1, networkObserver.onObserversCount) + networkObserver.unsubscribe(o1) + assertEquals(1, networkObserver.onObserversCount) + networkObserver.unsubscribe(o1) + assertEquals(1, networkObserver.onObserversCount) + networkObserver.unsubscribe(o2) + assertEquals(0, networkObserver.onObserversCount) + } + + @Test + fun givenRemoveObserver_whenNoObservers_thenOnObserversEmptyNotInvoked() { + assertEquals(0, networkObserver.onObserversCount) + networkObserver.unsubscribe { } + assertEquals(0, networkObserver.onObserversCount) + } + + @Test + fun givenObserver_whenAddMultipleTimes_thenOnlyRegisteredOnce() { + var invocations = 0 + val o1 = Observer { invocations++ } + networkObserver.subscribe(o1) + networkObserver.subscribe(o1) + networkObserver.notifyInvoke() + assertEquals(1, invocations) + networkObserver.unsubscribe(o1) + networkObserver.notifyInvoke() + assertEquals(1, invocations) + } +} diff --git a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorRuntimeEnvironmentUnitTest.kt b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorRuntimeEnvironmentUnitTest.kt new file mode 100644 index 000000000..c2f5ec06e --- /dev/null +++ b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorRuntimeEnvironmentUnitTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.kmp.tor.runtime + +import io.matthewnelson.kmp.file.absoluteFile +import io.matthewnelson.kmp.file.resolve +import io.matthewnelson.kmp.file.toFile +import io.matthewnelson.kmp.tor.core.api.ResourceInstaller +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.fail + +class TorRuntimeEnvironmentUnitTest { + + @Test + fun givenSameWorkDir_whenEnvironmentBuilder_thenReturnsSameInstance() { + val work = "".toFile().absoluteFile + val torResource = object : ResourceInstaller(work) { + override fun install(): Paths.Tor { fail() } + } + + val env1 = TorRuntime.Environment.Builder(work, work.resolve("cache")) { torResource } + val env2 = TorRuntime.Environment.Builder(work, work.resolve("cache2")) { torResource } + assertEquals(env1, env2) + + val env3 = TorRuntime.Environment.Builder(work.resolve("work"), work.resolve("cache")) { torResource } + assertNotEquals(env1, env3) + } +} diff --git a/library/runtime/src/jvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-JvmPlatform.kt b/library/runtime/src/jvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-JvmPlatform.kt new file mode 100644 index 000000000..96efd7a1b --- /dev/null +++ b/library/runtime/src/jvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-JvmPlatform.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +@file:Suppress("KotlinRedundantDiagnosticSuppress") + +package io.matthewnelson.kmp.tor.runtime.internal + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.core.resource.OSInfo +import io.matthewnelson.kmp.tor.runtime.TorRuntime +import java.security.MessageDigest + +@JvmSynthetic +@OptIn(InternalKmpTorApi::class) +@Throws(IllegalStateException::class) +internal actual fun TorRuntime.ServiceFactory.Companion.serviceRuntimeOrNull( + block: () -> TorRuntime.ServiceFactory, +): TorRuntime? { + val create = AndroidTorRuntimeCreate ?: return null + return create.invoke(null, block()) as TorRuntime +} + +@OptIn(InternalKmpTorApi::class) +private val AndroidTorRuntimeCreate by lazy { + if (!OSInfo.INSTANCE.isAndroidRuntime()) return@lazy null + + try { + Class + .forName("io.matthewnelson.kmp.tor.runtime.mobile.TorService\$AndroidTorRuntime") + ?.getMethod("create", TorRuntime.ServiceFactory::class.java) + } catch (_: Throwable) { + null + } +} + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun ByteArray.sha256(): String { + return MessageDigest + .getInstance("SHA-256") + .digest(this) + .encodeToString(Base16) +} diff --git a/library/runtime/src/nonJvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-NonJvmPlatform.kt b/library/runtime/src/nonJvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-NonJvmPlatform.kt new file mode 100644 index 000000000..9cbcb4b23 --- /dev/null +++ b/library/runtime/src/nonJvmMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/-NonJvmPlatform.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +@file:Suppress("KotlinRedundantDiagnosticSuppress") + +package io.matthewnelson.kmp.tor.runtime.internal + +import io.matthewnelson.encoding.base16.Base16 +import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi +import io.matthewnelson.kmp.tor.runtime.TorRuntime +import org.kotlincrypto.hash.sha2.SHA256 + +@OptIn(InternalKmpTorApi::class) +@Throws(IllegalStateException::class) +internal actual fun TorRuntime.ServiceFactory.Companion.serviceRuntimeOrNull( + block: () -> TorRuntime.ServiceFactory, +): TorRuntime? = null + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun ByteArray.sha256(): String { + return SHA256() + .digest(this) + .encodeToString(Base16) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 2c87ce391..638ae9479 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,7 @@ if (CHECK_PUBLICATION != null) { "runtime", "runtime-ctrl-api", "runtime-ctrl", + "runtime-mobile", ).forEach { name -> include(":library:$name") }