Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/feature/ios-device-provider' int…
Browse files Browse the repository at this point in the history
…o feature/ios-device-provider
  • Loading branch information
Malinskiy committed Oct 18, 2024
2 parents 6848c32 + 2074a45 commit 608bdc3
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()) {
Expand All @@ -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<Marathondevices>(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
}
}
}
}

Expand All @@ -183,6 +174,48 @@ class AppleSimulatorProvider(
}
}

private suspend fun readSocket(bufferWrapper: ByteBufferWrapper, socket: Socket, channel: SendChannel<Marathondevices>) {
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<Marathondevices>(devicesWithEnvironmentVariablesReplaced)
} catch (e: JsonMappingException) {
throw NoDevicesException("Invalid Marathondevices update format", e)
}
channel.send(marathonfile)
}
}

private fun startStaticProvider(marathondevices: Marathondevices): Job {
return launch {
processUpdate(marathondevices)
Expand All @@ -194,7 +227,7 @@ class AppleSimulatorProvider(
}

private suspend fun reconnect() {
var recreate = mutableSetOf<AppleDevice>()
val recreate = mutableSetOf<AppleDevice>()
devices.values.forEach { device ->
if (!device.commandExecutor.connected) {
channel.send(DeviceProvider.DeviceEvent.DeviceDisconnected(device))
Expand Down Expand Up @@ -499,3 +532,7 @@ class AppleSimulatorProvider(
channel.send(element = DeviceProvider.DeviceEvent.DeviceDisconnected(device))
}
}

private data class ByteBufferWrapper(
var buffer: ByteBuffer,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.malinskiy.marathon.log.MarathonLogging
import java.util.concurrent.ConcurrentHashMap

class DeviceTracker {

private val workers: ConcurrentHashMap<String, WorkerTracker> = ConcurrentHashMap()

fun update(marathondevices: Marathondevices): Map<Transport, List<TrackingUpdate>> {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 608bdc3

Please sign in to comment.