diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 3c890201b..370a2ea10 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -23,6 +23,7 @@ object Versions { val apacheCommonsText = "1.12.0" val apacheCommonsIO = "2.17.0" val okhttp = "4.12.0" + val ktor = "2.3.6" val influxDbClient = "2.24" val influxDb2Client = "7.2.0" val clikt = "5.0.0" @@ -95,6 +96,7 @@ object Libraries { val bugsnag = "com.bugsnag:bugsnag:${Versions.bugsnag}" val kotlinProcess = "com.github.pgreze:kotlin-process:${Versions.kotlinProcess}" val okhttp = "com.squareup.okhttp3:okhttp:${Versions.okhttp}" + val ktorNetwork = "io.ktor:ktor-network:${Versions.ktor}" } object TestLibraries { diff --git a/vendor/vendor-apple/ios/build.gradle.kts b/vendor/vendor-apple/ios/build.gradle.kts index 85ecf4622..3390b9a2e 100644 --- a/vendor/vendor-apple/ios/build.gradle.kts +++ b/vendor/vendor-apple/ios/build.gradle.kts @@ -7,6 +7,7 @@ plugins { dependencies { implementation(project(":vendor:vendor-apple:base")) + implementation(Libraries.ktorNetwork) testImplementation(TestLibraries.kluent) testImplementation(TestLibraries.assertk) diff --git a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/AppleSimulatorProvider.kt b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/AppleSimulatorProvider.kt index 8a7815d2e..26b1821b1 100644 --- a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/AppleSimulatorProvider.kt +++ b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/AppleSimulatorProvider.kt @@ -18,6 +18,9 @@ import com.malinskiy.marathon.apple.configuration.AppleTarget import com.malinskiy.marathon.apple.configuration.Marathondevices import com.malinskiy.marathon.apple.configuration.Transport import com.malinskiy.marathon.apple.device.ConnectionFactory +import com.malinskiy.marathon.apple.ios.extensions.compatClear +import com.malinskiy.marathon.apple.ios.extensions.compatLimit +import com.malinskiy.marathon.apple.ios.extensions.compatRewind import com.malinskiy.marathon.config.Configuration import com.malinskiy.marathon.config.vendor.VendorConfiguration import com.malinskiy.marathon.config.vendor.apple.DeviceProvider.Static @@ -26,8 +29,16 @@ import com.malinskiy.marathon.device.DeviceProvider import com.malinskiy.marathon.exceptions.NoDevicesException import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.time.Timer +import io.ktor.network.selector.ActorSelectorManager +import io.ktor.network.sockets.InetSocketAddress +import io.ktor.network.sockets.aSocket +import io.ktor.network.sockets.isClosed +import io.ktor.network.sockets.openReadChannel +import io.ktor.util.decodeString +import io.ktor.utils.io.core.use import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async @@ -47,9 +58,14 @@ import kotlinx.coroutines.withContext import org.apache.commons.text.StringSubstitutor import org.apache.commons.text.lookup.StringLookupFactory import java.io.File +import java.nio.ByteBuffer import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.CoroutineContext +import kotlin.math.ceil +import kotlin.math.log2 +import kotlin.math.pow +import kotlin.math.roundToInt //Should not use udid as key to support multiple devices with the same udid across transports typealias AppleDeviceId = String @@ -112,20 +128,52 @@ class AppleSimulatorProvider( monitoringJob = if (initialMarathonfile != null) { startStaticProvider(initialMarathonfile) } else { - startDynamicProvider() + val dynamicConfiguration = + vendorConfiguration.deviceProvider as com.malinskiy.marathon.config.vendor.apple.DeviceProvider.Dynamic + startDynamicProvider(dynamicConfiguration) } } - private fun startDynamicProvider(): Job { + private fun startDynamicProvider(dynamicConfiguration: com.malinskiy.marathon.config.vendor.apple.DeviceProvider.Dynamic): Job { return launch { + var buffer = ByteBuffer.allocate(4096) while (isActive) { sourceMutex.withLock { sourceChannel = produce { - //TODO: dynamic provider + aSocket(ActorSelectorManager(Dispatchers.IO)).tcp() + .connect(InetSocketAddress(dynamicConfiguration.host, dynamicConfiguration.port)).use { socket -> + while (!socket.isClosed) { + val readChannel = socket.openReadChannel() + val length = readChannel.readInt() + buffer.compatClear() + if (length <= buffer.capacity()) { + buffer.compatLimit(length) + } else { + //Reallocate up to a maximum of 1Mb + val newCapacity = ceil(log2(length.toDouble())).roundToInt() + if (newCapacity in 1..20) { + buffer = ByteBuffer.allocate(2.0.pow(newCapacity.toDouble()).toInt()) + buffer.compatLimit(length) + } else { + throw RuntimeException("Device provider update too long: maximum is 2^20 bytes") + } + } + readChannel.readFully(buffer) + buffer.compatRewind() + val devicesWithEnvironmentVariablesReplaced = + environmentVariableSubstitutor.replace(buffer.decodeString()) + val marathonfile = try { + objectMapper.readValue(devicesWithEnvironmentVariablesReplaced) + } catch (e: JsonMappingException) { + throw NoDevicesException("Invalid Marathondevices update format", e) + } + send(marathonfile) + } + } } } - while(true) { + while (true) { val channelResult = sourceChannel.tryReceive() channelResult.onSuccess { processUpdate(it) } reconnect() diff --git a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/extensions/ByteBuffer.kt b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/extensions/ByteBuffer.kt new file mode 100644 index 000000000..65a443542 --- /dev/null +++ b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/extensions/ByteBuffer.kt @@ -0,0 +1,15 @@ +package com.malinskiy.marathon.apple.ios.extensions + +import java.nio.Buffer +import java.nio.ByteBuffer + +/** + * Mitigation of running JDK 9 code on JRE 8 + * + * java.lang.NoSuchMethodError: java.nio.ByteBuffer.xxx()Ljava/nio/ByteBuffer; + */ +fun ByteBuffer.compatRewind() = ((this as Buffer).rewind() as ByteBuffer) +fun ByteBuffer.compatLimit(newLimit: Int) = ((this as Buffer).limit(newLimit) as ByteBuffer) +fun ByteBuffer.compatPosition(newLimit: Int) = ((this as Buffer).position(newLimit) as ByteBuffer) +fun ByteBuffer.compatFlip() = ((this as Buffer).flip() as ByteBuffer) +fun ByteBuffer.compatClear() = ((this as Buffer).clear() as ByteBuffer)