From 28d40dd4912335d5122cc05f68de5024f60957a5 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Mon, 2 Dec 2024 14:46:08 -0500 Subject: [PATCH 1/4] Update kmp-tor-resource test dependency to 408.13.0 --- .kotlin-js-store/yarn.lock | 68 +++++++++---------- gradle/libs.versions.toml | 2 +- .../kmp/tor/runtime/TorRuntime.kt | 2 +- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.kotlin-js-store/yarn.lock b/.kotlin-js-store/yarn.lock index 5890f9dfd..6ac1eb199 100644 --- a/.kotlin-js-store/yarn.lock +++ b/.kotlin-js-store/yarn.lock @@ -288,40 +288,40 @@ js-yaml@4.1.0: dependencies: argparse "^2.0.1" -kmp-tor.resource-exec-tor.all@408.12.0: - version "408.12.0" - resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.all/-/kmp-tor.resource-exec-tor.all-408.12.0.tgz#10c365fb3452ba8f5dd8f5be5a9a68ae8d813d81" - integrity sha512-mproBhkmswFep755NswyxBwQjBu+Trn8eML2oTXq+6z3fdu1n+fPhhnoak1gWJOTIizLpxnvO5hHHmH0ag3cOg== - dependencies: - kmp-tor.resource-exec-tor.linux-android "408.12.0" - kmp-tor.resource-exec-tor.linux-libc "408.12.0" - kmp-tor.resource-exec-tor.macos "408.12.0" - kmp-tor.resource-exec-tor.mingw "408.12.0" - -kmp-tor.resource-exec-tor.linux-android@408.12.0: - version "408.12.0" - resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.linux-android/-/kmp-tor.resource-exec-tor.linux-android-408.12.0.tgz#55cb915f8976ae53c965b91e1e81338c769814b4" - integrity sha512-T6uSnvpr7woLt86V1rYElLS1t6cVX4Uzvq2K3twlScI9WvrKNk+8579eLB63uYIRmT8XG+65mqp2uZqy4rIiNg== - -kmp-tor.resource-exec-tor.linux-libc@408.12.0: - version "408.12.0" - resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.linux-libc/-/kmp-tor.resource-exec-tor.linux-libc-408.12.0.tgz#174247250eec49b94ff53d23328c0be823cd1fc2" - integrity sha512-bhRQAK/+z9xtyeKWdVNdcLbothr4qRyhAcTsuJEVhS1rMUo0nfQ+ScbJUoE4LDDXQCg0kiZE3qPK6qZxk1DuCw== - -kmp-tor.resource-exec-tor.macos@408.12.0: - version "408.12.0" - resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.macos/-/kmp-tor.resource-exec-tor.macos-408.12.0.tgz#bdddb7add33af2763da87940a4f234cc42d828a1" - integrity sha512-q45vZwiii9Yu6ql80YezM6E82HSD8Og9vUvGNP+oIr97wnHe6WoAxOk9oaSMMOExi0ZkQG6YXntqfcJS5hj4KQ== - -kmp-tor.resource-exec-tor.mingw@408.12.0: - version "408.12.0" - resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.mingw/-/kmp-tor.resource-exec-tor.mingw-408.12.0.tgz#5ad4fb58eddcb71207e1ab4b052f9b7b1f8bdc45" - integrity sha512-ycvSEwG7snWCOtfC2jU6mP3n1mTpqFTOyiNjs1FlwYhZRiSDWE+bR0DgZdyRDkwKPlyLioQGbl/x5zjBnS0acw== - -kmp-tor.resource-geoip@408.12.0: - version "408.12.0" - resolved "https://registry.yarnpkg.com/kmp-tor.resource-geoip/-/kmp-tor.resource-geoip-408.12.0.tgz#02c15bb53a174658add7b38dea5d45731535cfbd" - integrity sha512-Sg/mmEo58a6oNo9+W1ygDyIS8KH+1JXTv78Wiyed7D8gIQx4Bn2QXC7jpSH6pN69/OG/CBC7YMRDXTpdACczEA== +kmp-tor.resource-exec-tor.all@408.13.0: + version "408.13.0" + resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.all/-/kmp-tor.resource-exec-tor.all-408.13.0.tgz#6e37151c29ff286fcf59a34db2b764ab2561dfa7" + integrity sha512-7oOKoAhllo6xP947ygXgulycZBnTnRf9p9/9eLvScwS5AMcR2MTQYjGIL+a+iUXQs4xKTfa9mMCf3Ei8JSJLPg== + dependencies: + kmp-tor.resource-exec-tor.linux-android "408.13.0" + kmp-tor.resource-exec-tor.linux-libc "408.13.0" + kmp-tor.resource-exec-tor.macos "408.13.0" + kmp-tor.resource-exec-tor.mingw "408.13.0" + +kmp-tor.resource-exec-tor.linux-android@408.13.0: + version "408.13.0" + resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.linux-android/-/kmp-tor.resource-exec-tor.linux-android-408.13.0.tgz#32b39c6c0b41ec8a02aa14aeb46f26648547e817" + integrity sha512-XDOipQAQJxtt41B8bRsk/nGDYJ1ItqFi++xSQKlfSkzZRbfh+ZIjyAWbe+EIOSTe1SzWgKRMGQVTV3Yq0sPSvw== + +kmp-tor.resource-exec-tor.linux-libc@408.13.0: + version "408.13.0" + resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.linux-libc/-/kmp-tor.resource-exec-tor.linux-libc-408.13.0.tgz#de75df9042ef29408cf417d3a8bec9aea3789859" + integrity sha512-K7E9/bSE85VyeaPjuZVfxWvVzYeEMTFNvmNwnOk/1CeT/gJYgNTkh7OThX2TTmmHGAEGpXYhr22pLA76dxP8Bw== + +kmp-tor.resource-exec-tor.macos@408.13.0: + version "408.13.0" + resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.macos/-/kmp-tor.resource-exec-tor.macos-408.13.0.tgz#44bc56cbf559c8bed4ab163604ddd7c91f570511" + integrity sha512-JGsd24e4SoMBf5cMYJFAbZG4D66AODG9fCEwdF7xqfjgmsUVmNbzUyR0W3kFeHz4n1AdymnhxcE3kptEhSAGPg== + +kmp-tor.resource-exec-tor.mingw@408.13.0: + version "408.13.0" + resolved "https://registry.yarnpkg.com/kmp-tor.resource-exec-tor.mingw/-/kmp-tor.resource-exec-tor.mingw-408.13.0.tgz#df4e5acf63b2ca14c005aecfe2aa52275597e4e0" + integrity sha512-1Aeyl09Tp+629xFOQlPoULxNEIZlVoY9z/dG4QG270Qmss+QLpaWbHJ1IMIeqzSZmrTg73rvFjLSwuYDm3muSg== + +kmp-tor.resource-geoip@408.13.0: + version "408.13.0" + resolved "https://registry.yarnpkg.com/kmp-tor.resource-geoip/-/kmp-tor.resource-geoip-408.13.0.tgz#57f94a356782c2372a7ba67bd05148b6db0f42eb" + integrity sha512-6lxdb86o7J91sl8qcJn6e/VeGPtbgUG+IPRaw+1gALCTKdpjGpUrKDYY2Nk23bo+DOAGmgg24Pv9nTv9AcH4Jg== locate-path@^6.0.0: version "6.0.0" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a5a857ec..b51e17938 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ kotlinx-coroutines = "1.8.1" androidx-test-core = "1.5.0" androidx-test-runner = "1.5.2" -kmp-tor-resource = "408.12.0" +kmp-tor-resource = "408.13.0" ktor = "2.3.11" okio = "3.7.0" 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 index bbf78a724..b6845c269 100644 --- 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 @@ -120,7 +120,7 @@ public sealed interface TorRuntime: * **NOTE:** This should be a singleton with **no** contextual or * non-singleton references (such as Android Activity Context). * - * **NOTE:** If utilizing the `kmp-tor-mobile` dependency, its + * **NOTE:** If utilizing the `runtime-service` dependency, its * own observer implementation will be favored over this setting. * */ @JvmField From 2f7db8436780b69e16389e5ed938980337f59126 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Mon, 2 Dec 2024 21:26:12 -0500 Subject: [PATCH 2/4] Add check for empty listeners when TorState is not On or Starting --- .../kmp/tor/runtime/TorListeners.kt | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorListeners.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorListeners.kt index a4417385e..ab43cc890 100644 --- a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorListeners.kt +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorListeners.kt @@ -361,14 +361,14 @@ public class TorListeners private constructor( protected abstract fun notify(state: TorState) final override fun onListenerConfChange(type: String, changes: Set) { - val executable = when (Type.valueOfOrNull(type)) { + val diff = when (Type.valueOfOrNull(type)) { is Type.SOCKS -> Executable { Type.SOCKS.diffUnixListeners(_listeners.socksUnix, changes) } else -> return } - synchronized(lock) { executable.execute() } + synchronized(lock) { diff.execute() } } final override fun update(type: String, address: String, wasClosed: Boolean) { @@ -409,41 +409,62 @@ public class TorListeners private constructor( } protected override fun notify(old: TorState, new: TorState) { - val listeners = synchronized(lock) { with(_listeners) { - // on -> NOT on - if (old.daemon.isOn && !new.daemon.isOn) { - return@with EMPTY - } - - if (new.daemon.isBootstrapped) { - - // enabled -> disabled - if (old.network.isEnabled && new.network.isDisabled) { - return@with EMPTY + var notify = true + + val listeners = synchronized(lock) { + + val update = run { + // If NOT on or starting + if (!(new.daemon.isOn || new.daemon.isStarting)) { + // ensure current listeners are EMPTY + return@run if (_listeners.isEmpty) { + null + } else { + // Have non-empty TorListeners that must be cleared. + + // If subscribed observers were previously notified of + // something, need to inform them of the change. + notify = old.daemon.isBootstrapped + EMPTY + } } - if (new.network.isEnabled) { + if (new.daemon.isBootstrapped) { - // disabled -> enabled - if (old.network.isDisabled) { - return@with this + // enabled -> disabled + if (old.network.isEnabled && new.network.isDisabled) { + return@run EMPTY } - // NOT bootstrapped -> bootstrapped - if (!old.daemon.isBootstrapped) { - return@with this + if (new.network.isEnabled) { + + // disabled -> enabled + if (old.network.isDisabled) { + return@run _listeners + } + + // NOT bootstrapped -> bootstrapped + if (!old.daemon.isBootstrapped) { + return@run _listeners + } } } + + null + } + + if (update != null) { + _notifyJob?.cancel() + _listeners = update } - null - }?.also { - _notifyJob?.cancel() - _listeners = it - } } + update + } notify(new) - listeners?.let { notify(it) } + if (notify && listeners != null) { + notify(listeners) + } } private fun Type.onClose(address: String) = when (this) { @@ -609,10 +630,7 @@ public class TorListeners private constructor( } private fun Type.diffUnixListeners(listeners: Set, changes: Set) { - when (this) { - Type.SOCKS -> {} - else -> return - } + if (this !is Type.SOCKS) return with(state) { if (!(daemon.isOn || daemon.isStarting)) return From 1d42faaef77d2a481e839182539435f00b1e039f Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Mon, 2 Dec 2024 21:26:19 -0500 Subject: [PATCH 3/4] Clean up code --- .../kmp/tor/runtime/TorListeners.kt | 85 +++++++++---------- .../internal/observer/ObserverConfChanged.kt | 2 +- .../runtime/TorListenersManagerUnitTest.kt | 31 +++++-- .../observer/ObserverConfChangedUnitTest.kt | 6 +- .../runtime/test/TestTorListenersManager.kt | 6 +- 5 files changed, 72 insertions(+), 58 deletions(-) diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorListeners.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorListeners.kt index ab43cc890..ed20e9ffc 100644 --- a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorListeners.kt +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/TorListeners.kt @@ -319,7 +319,7 @@ public class TorListeners private constructor( } internal interface Manager: TorState.Manager { - fun onListenerConfChange(type: String, changes: Set) + fun onSocksConfChange(changes: Set) fun update(type: String, address: String, wasClosed: Boolean) } @@ -360,15 +360,43 @@ public class TorListeners private constructor( protected abstract fun notify(listeners: TorListeners) protected abstract fun notify(state: TorState) - final override fun onListenerConfChange(type: String, changes: Set) { - val diff = when (Type.valueOfOrNull(type)) { - is Type.SOCKS -> Executable { - Type.SOCKS.diffUnixListeners(_listeners.socksUnix, changes) + final override fun onSocksConfChange(changes: Set) { + synchronized(lock) { + val current = _listeners.socksUnix + + with(state) { + if (!(daemon.isOn || daemon.isStarting)) return + if (network.isDisabled) return + } + if (current.isEmpty()) return + + val diffs = LinkedHashMap(current.size, 1.0F) + + current.forEach { file -> + var wasClosed = true + + for (change in changes) { + // Change could be a TCP port or socket address, a + // quoted or unquoted path prefixed with "unix:", contain + // optionals appended to it, etc. + // + // The simplest way to check which unix listeners have + // been closed is to see if any of the changes contain + // the absolute path string. If it does, then it was NOT + // closed. + if (change.contains(file.path)) { + wasClosed = false + break + } + } + + diffs[file] = wasClosed } - else -> return - } - synchronized(lock) { diff.execute() } + diffs.forEach { (file, wasClosed) -> + updateNoLock(type = Type.SOCKS, file.path, wasClosed) + } + } } final override fun update(type: String, address: String, wasClosed: Boolean) { @@ -408,7 +436,7 @@ public class TorListeners private constructor( } } - protected override fun notify(old: TorState, new: TorState) { + protected final override fun notify(old: TorState, new: TorState) { var notify = true val listeners = synchronized(lock) { @@ -582,7 +610,7 @@ public class TorListeners private constructor( // close all other socks listeners that may be // open, but will not dispatch the CONF_CHANGED // event. - onListenerConfChange("Socks", setOf("9050")) + onSocksConfChange(setOf("9050")) } }) } @@ -600,7 +628,7 @@ public class TorListeners private constructor( } if (changes != null) { - onListenerConfChange("Socks", changes) + onSocksConfChange(changes) } }) } @@ -629,41 +657,6 @@ public class TorListeners private constructor( } } - private fun Type.diffUnixListeners(listeners: Set, changes: Set) { - if (this !is Type.SOCKS) return - - with(state) { - if (!(daemon.isOn || daemon.isStarting)) return - if (network.isDisabled) return - } - if (listeners.isEmpty()) return - - val diffs = LinkedHashMap(listeners.size, 1.0F) - - listeners.forEach { file -> - var wasClosed = true - - for (change in changes) { - // Change could be a TCP port or socket address, a - // quoted or unquoted path prefixed with "unix:", contain - // optionals appended to it, etc. - // - // The simplest way to check which unix listeners have - // been closed is to see if any of the changes contain - // the absolute path string. If it does, then it was NOT - // closed. - if (change.contains(file.path)) { - wasClosed = false - break - } - } - - diffs[file] = wasClosed - } - - diffs.forEach { (file, wasClosed) -> updateNoLock(type = this, file.path, wasClosed) } - } - private sealed interface Copy { fun invoke(new: MutableSet): TorListeners diff --git a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/observer/ObserverConfChanged.kt b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/observer/ObserverConfChanged.kt index 104c3a088..0bb41d874 100644 --- a/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/observer/ObserverConfChanged.kt +++ b/library/runtime/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/internal/observer/ObserverConfChanged.kt @@ -68,7 +68,7 @@ internal open class ObserverConfChanged internal constructor( } if (socks != null) { - manager.onListenerConfChange("Socks", socks) + manager.onSocksConfChange(socks) } } } diff --git a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorListenersManagerUnitTest.kt b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorListenersManagerUnitTest.kt index 116f2becd..91b1e44d8 100644 --- a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorListenersManagerUnitTest.kt +++ b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorListenersManagerUnitTest.kt @@ -69,8 +69,13 @@ class TorListenersManagerUnitTest { fun givenListeners_whenEmpty_thenIsEmptyTrue() { val listeners = TorListeners() assertTrue(listeners.isEmpty) + assertEquals(0, listeners.dir.size) assertEquals(0, listeners.dns.size) assertEquals(0, listeners.http.size) + assertEquals(0, listeners.metrics.size) + assertEquals(0, listeners.natd.size) + assertEquals(0, listeners.or.size) + assertEquals(0, listeners.orExt.size) assertEquals(0, listeners.socks.size) assertEquals(0, listeners.socksUnix.size) assertEquals(0, listeners.trans.size) @@ -80,11 +85,27 @@ class TorListenersManagerUnitTest { fun givenListeners_whenNotEmpty_thenIsEmptyFalse() { val empty = TorListeners() assertTrue(empty.isEmpty) - assertFalse(empty.copy(dns = setOf(address)).isEmpty) - assertFalse(empty.copy(http = setOf(address)).isEmpty) - assertFalse(empty.copy(socks = setOf(address)).isEmpty) - assertFalse(empty.copy(socksUnix = setOf(SysTempDir)).isEmpty) - assertFalse(empty.copy(trans = setOf(address)).isEmpty) + + fun TorListeners.assertIsNotEmpty(copyTarget: TorListeners.() -> Set<*>) { + assertFalse(isEmpty) + + val target = copyTarget(this) + assertTrue(target.isNotEmpty()) + + val all = listOf(dir, dns, http, metrics, natd, or, orExt, socks, socksUnix, trans).flatten() + assertEquals(target.size, all.size) + } + + empty.copy(dir = setOf(address)).assertIsNotEmpty { dir } + empty.copy(dns = setOf(address)).assertIsNotEmpty { dns } + empty.copy(http = setOf(address)).assertIsNotEmpty { http } + empty.copy(metrics = setOf(address)).assertIsNotEmpty { metrics } + empty.copy(natd = setOf(address)).assertIsNotEmpty { natd } + empty.copy(or = setOf(address)).assertIsNotEmpty { or } + empty.copy(orExt = setOf(address)).assertIsNotEmpty { orExt } + empty.copy(socks = setOf(address)).assertIsNotEmpty { socks } + empty.copy(socksUnix = setOf(SysTempDir)).assertIsNotEmpty { socksUnix } + empty.copy(trans = setOf(address)).assertIsNotEmpty { trans } } @Test diff --git a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/internal/observer/ObserverConfChangedUnitTest.kt b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/internal/observer/ObserverConfChangedUnitTest.kt index cfbcdebf2..fc2171ee4 100644 --- a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/internal/observer/ObserverConfChangedUnitTest.kt +++ b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/internal/observer/ObserverConfChangedUnitTest.kt @@ -80,8 +80,8 @@ class ObserverConfChangedUnitTest { observer.notify("SocksPort=$expected4") assertEquals(3, observer.manager.unixConf.size) - assertEquals("Socks" to setOf(expected1), observer.manager.unixConf[0]) - assertEquals("Socks" to setOf(expected2, expected3), observer.manager.unixConf[1]) - assertEquals("Socks" to setOf(expected4), observer.manager.unixConf[2]) + assertEquals(setOf(expected1), observer.manager.unixConf[0]) + assertEquals(setOf(expected2, expected3), observer.manager.unixConf[1]) + assertEquals(setOf(expected4), observer.manager.unixConf[2]) } } diff --git a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/test/TestTorListenersManager.kt b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/test/TestTorListenersManager.kt index aea3a0dc7..415a62865 100644 --- a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/test/TestTorListenersManager.kt +++ b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/test/TestTorListenersManager.kt @@ -22,9 +22,9 @@ internal class TestTorListenersManager: TorListeners.Manager { val states = mutableListOf>() val listeners = mutableListOf>() - val unixConf = mutableListOf>>() - override fun onListenerConfChange(type: String, changes: Set) { - unixConf.add(type to changes) + val unixConf = mutableListOf>() + override fun onSocksConfChange(changes: Set) { + unixConf.add(changes) } override fun update(daemon: TorState.Daemon?, network: TorState.Network?) { states.add(daemon to network) From 86630b6d7768b0a38c4b01be55b89c52f1937a44 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 3 Dec 2024 04:51:01 -0500 Subject: [PATCH 4/4] Remove redundant test --- README.md | 46 +++-- .../runtime/ctrl/TorCtrlFactoryUnitTest.kt | 157 ------------------ 2 files changed, 32 insertions(+), 171 deletions(-) delete mode 100644 library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/TorCtrlFactoryUnitTest.kt diff --git a/README.md b/README.md index 72fad188b..bccd23c19 100644 --- a/README.md +++ b/README.md @@ -24,46 +24,64 @@ Kotlin Multiplatform support for embedding Tor into your application. ```kotlin -val myTorRuntime = TorRuntime.Builder(myTorEnvironment) { +val runtime = TorRuntime.Builder(myEnvironment) { RuntimeEvent.entries().forEach { event -> - observerStatic(event, OnEvent.Executor.Immediate) { data -> println(data.toString()) } + observerStatic(event, OnEvent.Executor.Immediate) { data -> + println(data.toString()) + } } TorEvent.entries().forEach { event -> - observerStatic(event, OnEvent.Executor.Immediate) { data -> println(data) } + observerStatic(event, OnEvent.Executor.Immediate) { data -> + println(data) + } } + config { environment -> + TorOption.SocksPort.configure { auto() } + // ... + } + required(TorEvent.ERR) + required(TorEvent.WARN) } ``` ```kotlin // Asynchronous APIs myScope.launch { - myTorRuntime.startDaemonAsync() - myTorRuntime.restartDaemonAsync() - myTorRuntime.stopDaemonAsync() + runtime.startDaemonAsync() + runtime.restartDaemonAsync() + runtime.executeAsync(TorCmd.Signal.NewNym) + runtime.stopDaemonAsync() } ``` ```kotlin // Synchronous APIs (Android/Jvm/Native) -myTorRuntime.startDaemonSync() -myTorRuntime.restartDaemonSync() -myTorRuntime.stopDaemonSync() +runtime.startDaemonSync() +runtime.restartDaemonSync() +runtime.executeSync(TorCmd.Signal.NewNym) +runtime.stopDaemonSync() ``` ```kotlin // Callback APIs -myTorRuntime.enqueue( +runtime.enqueue( Action.StartDaemon, OnFailure.noOp(), OnSuccess { - myTorRuntime.enqueue( + runtime.enqueue( Action.RestartDaemon, OnFailure.noOp(), OnSuccess { - myTorRuntime.enqueue( - Action.StopDaemon, + runtime.enqueue( + TorCmd.Signal.NewNym, OnFailure.noOp(), - OnSuccess.noOp(), + OnSuccess { + runtime.enqueue( + Action.StopDaemon, + OnFailure.noOp(), + OnSuccess.noOp(), + ) + }, ) }, ) diff --git a/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/TorCtrlFactoryUnitTest.kt b/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/TorCtrlFactoryUnitTest.kt deleted file mode 100644 index 231f6e398..000000000 --- a/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/TorCtrlFactoryUnitTest.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.ctrl - -import io.matthewnelson.kmp.file.resolve -import io.matthewnelson.kmp.tor.common.api.InternalKmpTorApi -import io.matthewnelson.kmp.tor.common.core.SynchronizedObject -import io.matthewnelson.kmp.tor.common.core.synchronized -import io.matthewnelson.kmp.tor.runtime.core.UncaughtException -import io.matthewnelson.kmp.tor.runtime.core.net.LocalHost -import io.matthewnelson.kmp.tor.runtime.core.net.Port -import io.matthewnelson.kmp.tor.runtime.core.net.Port.Ephemeral.Companion.toPortEphemeral -import io.matthewnelson.kmp.tor.runtime.core.net.IPSocketAddress -import io.matthewnelson.kmp.tor.runtime.core.config.TorOption -import io.matthewnelson.kmp.tor.runtime.core.util.findNextAvailableAsync -import kotlinx.coroutines.* -import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import kotlin.test.fail -import kotlin.time.Duration.Companion.milliseconds - -@OptIn(ExperimentalStdlibApi::class, InternalKmpTorApi::class) -class TorCtrlFactoryUnitTest { - - @Test - fun givenIPv4_whenConnect_thenIsSuccessful() = runTest { - LocalHost.IPv4.runTCPTest(9055.toPortEphemeral()) - } - - @Test - fun givenIPv6_whenConnect_thenIsSuccessful() = runTest { - LocalHost.IPv6.runTCPTest(9155.toPortEphemeral()) - } - - @Test - fun givenConnection_whenTorStops_thenDestroysItself() = runTest { - LocalHost.IPv4.runTCPTest(9255.toPortEphemeral()) { runtime, _ -> - runtime.close() - } - } - - @Test - fun givenUnixDomainSocket_whenConnect_thenIsSuccessful() = runTest { - val uds = LOADER.resourceDir - .resolve("data") - .resolve("ctrl.sock") - - val ctrlArg = try { - TorOption.__ControlPort.asSetting { - unixSocket(value = uds) - }.items.first().argument - } catch (_: UnsupportedOperationException) { - println("Skipping...") - return@runTest - } - - val runtime = startTor(ctrlArg) - val factory = TorCtrl.Factory(handler = UncaughtException.Handler.THROW) - - val ctrl = try { - try { - factory.connectAsync(uds) - } catch (_: Throwable) { - withContext(Dispatchers.Default) { delay(350.milliseconds) } - factory.connectAsync(uds) - } - } finally { - runtime.close() - } - - withContext(Dispatchers.Default) { delay(350.milliseconds) } - - assertTrue(ctrl.isDestroyed()) - } - - private suspend fun LocalHost.runTCPTest( - startPort: Port.Ephemeral, - block: suspend (runtime: AutoCloseable, ctrl: TorCtrl) -> Unit = { runtime, ctrl -> - // default test behavior is to disconnect ctrl listener first - ctrl.destroy() - runtime.close() - } - ) { - val debugLogs = mutableListOf() - val lock = SynchronizedObject() - val factory = TorCtrl.Factory( - debugger = { synchronized(lock) { debugLogs.add(it) } }, - handler = UncaughtException.Handler.THROW - ) - - val host = resolve() - val port = startPort.findNextAvailableAsync(100, this) - - val address = IPSocketAddress(host, port) - - val runtime = startTor(address.toString()) - - val ctrl = try { - factory.connectAsync(address) - } catch (_: Throwable) { - withContext(Dispatchers.Default) { delay(350.milliseconds) } - factory.connectAsync(address) - } - - val latch = Job(currentCoroutineContext().job) - var invocationDestroy = 0 - ctrl.invokeOnDestroy { - invocationDestroy++ - latch.complete() - } - - block(runtime, ctrl) - - latch.join() - - assertEquals(1, invocationDestroy) - - synchronized(lock) { - listOf( - "Starting Read", - "End Of Stream", - "Stopped Reading", - - // Ensures that, even if destroy was not called - // externally (e.g. Tor stopped and closed the - // connection), that TorCtrl.destroy was invoked - "Connection Closed", - - "Scope Cancelled", - ).forEach { log -> - - for (dLog in debugLogs) { - if (dLog.contains(log)) { - return@forEach - } - } - - fail("Debug logs did not contain $log") - } - } - } -}