diff --git a/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/DeviceProvider.kt b/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/DeviceProvider.kt index 2f39b4855..1790ad698 100644 --- a/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/DeviceProvider.kt +++ b/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/DeviceProvider.kt @@ -22,5 +22,5 @@ sealed class DeviceProvider { data class Dynamic( @JsonProperty("host") val host: String = "127.0.0.1", @JsonProperty("port") val port: Int = 5037, - ) + ) : DeviceProvider() } diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/xctestrun/v2/TestTarget.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/xctestrun/v2/TestTarget.kt index f9ab258e1..49b355c4d 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/xctestrun/v2/TestTarget.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/xctestrun/v2/TestTarget.kt @@ -279,5 +279,5 @@ class TestTarget : TestTarget { * Added in xcode 15, possible values are [screenshots, screenRecording]. Defaults to screenRecording * Ignored by previous versions of xcode */ - var preferredScreenCaptureFormat: String? by delegate.optionalDelegateFor("preferredScreenCaptureFormat") + var preferredScreenCaptureFormat: String? by delegate.optionalDelegateFor("PreferredScreenCaptureFormat") } diff --git a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/AppleSimulatorDevice.kt b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/AppleSimulatorDevice.kt index 8028b9ae4..b0a6e2656 100644 --- a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/AppleSimulatorDevice.kt +++ b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/AppleSimulatorDevice.kt @@ -190,7 +190,8 @@ class AppleSimulatorDevice( deviceFeatures = detectFeatures() - supportsTranscoding = executeWorkerCommand(listOf(vendorConfiguration.screenRecordConfiguration.videoConfiguration.transcoding.binary, "-version"))?.let { + val transcodingVideoConfiguration = vendorConfiguration.screenRecordConfiguration.videoConfiguration.transcoding + supportsTranscoding = transcodingVideoConfiguration.enabled && executeWorkerCommand(listOf(transcodingVideoConfiguration.binary, "-version"))?.let { if (it.successful) { true } else { 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 26b1821b1..5c5b9ef85 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 @@ -31,6 +31,7 @@ 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.Socket import io.ktor.network.sockets.aSocket import io.ktor.network.sockets.isClosed import io.ktor.network.sockets.openReadChannel @@ -45,6 +46,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.onSuccess import kotlinx.coroutines.channels.produce import kotlinx.coroutines.delay @@ -58,6 +60,7 @@ import kotlinx.coroutines.withContext import org.apache.commons.text.StringSubstitutor import org.apache.commons.text.lookup.StringLookupFactory import java.io.File +import java.net.ConnectException import java.nio.ByteBuffer import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -109,7 +112,8 @@ class AppleSimulatorProvider( override suspend fun initialize() { logger.debug("Initializing AppleSimulatorProvider") - //Fail fast if we use static provider with no devices available + // Fail fast if we use static provider with no devices available + // todo tell that the provider is static val file = vendorConfiguration.devicesFile ?: File(System.getProperty("user.dir"), "Marathondevices") var initialMarathonfile: Marathondevices? = null if (vendorConfiguration.deviceProvider is Static || file.exists()) { @@ -136,40 +140,27 @@ class AppleSimulatorProvider( private fun startDynamicProvider(dynamicConfiguration: com.malinskiy.marathon.config.vendor.apple.DeviceProvider.Dynamic): Job { return launch { - var buffer = ByteBuffer.allocate(4096) + val byteBufferWrapper = ByteBufferWrapper( + buffer = ByteBuffer.allocate(4096) + ) while (isActive) { sourceMutex.withLock { sourceChannel = produce { - 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") - } + while(true) { + try { + aSocket(ActorSelectorManager(Dispatchers.IO)) + .tcp() + .connect(InetSocketAddress(dynamicConfiguration.host, dynamicConfiguration.port)).use { socket -> + readSocket(byteBufferWrapper, socket, this) } - 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) - } + } catch (e: ConnectException) { + logger.warn { "Connection refused, retrying in 5 seconds..." } + delay(5000) // Retry delay if socket connection is refused + } catch (e: Exception) { + logger.error(e) { "Error occurred: ${e.message}" } + break // Exit the reading loop if a critical error occurs } + } } } @@ -183,6 +174,48 @@ class AppleSimulatorProvider( } } + private suspend fun readSocket(bufferWrapper: ByteBufferWrapper, socket: Socket, channel: SendChannel) { + val readChannel = socket.openReadChannel() + while (!socket.isClosed) { + // Read the 4 reserved bytes + val reservedBytes = ByteArray(4) + readChannel.readFully(reservedBytes, 0, 4) + + // Read message length (4 bytes) + val length = readChannel.readInt() + + bufferWrapper.buffer.compatClear() + if (length <= bufferWrapper.buffer.capacity()) { + bufferWrapper.buffer.compatLimit(length) + } else { + // Reallocate up to a maximum of 1Mb + val newCapacity = ceil(log2(length.toDouble())).roundToInt() + if (newCapacity in 1..20) { + bufferWrapper.buffer = ByteBuffer.allocate(2.0.pow(newCapacity.toDouble()).toInt()) + bufferWrapper.buffer.compatLimit(length) + } else { + throw RuntimeException("Device provider update too long: maximum is 2^20 bytes") + } + } + + // Read the full message into the buffer + readChannel.readFully(bufferWrapper.buffer) + + // Process the message + bufferWrapper.buffer.compatRewind() + val message = bufferWrapper.buffer.decodeString() + + val devicesWithEnvironmentVariablesReplaced = + environmentVariableSubstitutor.replace(message) + val marathonfile = try { + objectMapper.readValue(devicesWithEnvironmentVariablesReplaced) + } catch (e: JsonMappingException) { + throw NoDevicesException("Invalid Marathondevices update format", e) + } + channel.send(marathonfile) + } + } + private fun startStaticProvider(marathondevices: Marathondevices): Job { return launch { processUpdate(marathondevices) @@ -194,7 +227,7 @@ class AppleSimulatorProvider( } private suspend fun reconnect() { - var recreate = mutableSetOf() + val recreate = mutableSetOf() devices.values.forEach { device -> if (!device.commandExecutor.connected) { channel.send(DeviceProvider.DeviceEvent.DeviceDisconnected(device)) @@ -499,3 +532,7 @@ class AppleSimulatorProvider( channel.send(element = DeviceProvider.DeviceEvent.DeviceDisconnected(device)) } } + +private data class ByteBufferWrapper( + var buffer: ByteBuffer, +) diff --git a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/DeviceTracker.kt b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/DeviceTracker.kt index 51a6c172a..ac27bc76b 100644 --- a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/DeviceTracker.kt +++ b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/DeviceTracker.kt @@ -8,6 +8,7 @@ import com.malinskiy.marathon.log.MarathonLogging import java.util.concurrent.ConcurrentHashMap class DeviceTracker { + private val workers: ConcurrentHashMap = ConcurrentHashMap() fun update(marathondevices: Marathondevices): Map> { @@ -98,7 +99,9 @@ class WorkerTracker(val transport: Transport) { val target = desired.first() if (previousState == null) { devices[id] = DeviceState.PROVISIONED(target, desired.size.toUInt()) - updates.add(Pair(target, TrackingUpdate.Connected(target))) + repeat(desired.size) { + updates.add(Pair(target, TrackingUpdate.Connected(target))) + } } else { val desiredCount = desired.size.toUInt() val currentCount = (previousState as DeviceState.PROVISIONED).count