Skip to content

Commit 7953b11

Browse files
committed
feat(android): reconnect lost devices
1 parent 39fb827 commit 7953b11

File tree

10 files changed

+99
-18
lines changed

10 files changed

+99
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
package com.malinskiy.marathon.device
22

3-
import kotlinx.coroutines.channels.Channel
3+
import kotlinx.coroutines.channels.ReceiveChannel
44

55
interface DeviceProvider {
66
sealed class DeviceEvent {
7-
class DeviceConnected(val device: Device) : DeviceEvent()
8-
class DeviceDisconnected(val device: Device) : DeviceEvent()
7+
class DeviceConnected(
8+
val device: Device,
9+
) : DeviceEvent()
10+
11+
class DeviceDisconnected(
12+
val device: Device,
13+
) : DeviceEvent()
914
}
1015

1116
suspend fun initialize()
1217

1318
/**
14-
* Remote test parsers require a temp device
15-
* This method should be called before reading from the [subscribe()] channel
19+
* Remote test parsers require a temp device This method should be called before reading from
20+
* the [subscribe()] channel
1621
*/
17-
suspend fun borrow() : Device
22+
suspend fun borrow(): Device
23+
1824
suspend fun terminate()
19-
fun subscribe(): Channel<DeviceEvent>
25+
26+
fun subscribe(scheduler: ReceiveChannel<DeviceEvent>): ReceiveChannel<DeviceEvent>
2027
}

core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.malinskiy.marathon.config.Configuration
88
import com.malinskiy.marathon.device.Device
99
import com.malinskiy.marathon.device.DeviceInfo
1010
import com.malinskiy.marathon.device.DevicePoolId
11+
import com.malinskiy.marathon.device.DeviceProvider
1112
import com.malinskiy.marathon.device.toDeviceInfo
1213
import com.malinskiy.marathon.execution.bundle.TestBundleIdentifier
1314
import com.malinskiy.marathon.execution.device.DeviceActor
@@ -27,6 +28,7 @@ class DevicePoolActor(
2728
private val poolId: DevicePoolId,
2829
private val configuration: Configuration,
2930
private val poolProgressAccumulator: PoolProgressAccumulator,
31+
private val deviceProviderChannel: SendChannel<DeviceProvider.DeviceEvent>,
3032
analytics: Analytics,
3133
shard: TestShard,
3234
timer: Timer,
@@ -47,9 +49,14 @@ class DevicePoolActor(
4749
is DevicePoolMessage.FromQueue.Notify -> notifyDevices()
4850
is DevicePoolMessage.FromQueue.Terminated -> onQueueTerminated()
4951
is DevicePoolMessage.FromQueue.ExecuteBatch -> executeBatch(msg.device, msg.batch)
52+
is DevicePoolMessage.FromDevice.DeviceLost -> deviceLost(msg.device)
5053
}
5154
}
5255

56+
private suspend fun deviceLost(device: Device) {
57+
deviceProviderChannel.send(DeviceProvider.DeviceEvent.DeviceDisconnected(device))
58+
}
59+
5360
/**
5461
* Any problem with a device should not propagate a cancellation upstream
5562
*/

core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolMessage.kt

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ sealed class DevicePoolMessage {
1515
data class IsReady(override val device: Device) : FromDevice(device)
1616
data class CompletedTestBatch(override val device: Device, val results: TestBatchResults) : FromDevice(device)
1717
data class ReturnTestBatch(override val device: Device, val batch: TestBatch, val reason: String) : FromDevice(device)
18+
data class DeviceLost(override val device: Device) : FromDevice(device)
1819
}
1920

2021
sealed class FromQueue : DevicePoolMessage() {

core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.malinskiy.marathon.execution
22

3+
import com.malinskiy.marathon.actor.unboundedChannel
34
import com.malinskiy.marathon.analytics.external.Analytics
45
import com.malinskiy.marathon.analytics.internal.pub.Track
56
import com.malinskiy.marathon.config.Configuration
@@ -45,6 +46,7 @@ class Scheduler(
4546
private val pools = ConcurrentHashMap<DevicePoolId, DevicePoolActor>()
4647
private val results = ConcurrentHashMap<DevicePoolId, PoolProgressAccumulator>()
4748
private val poolingStrategy = configuration.poolingStrategy.toPoolingStrategy()
49+
private val deviceProviderChannel = unboundedChannel<DeviceProvider.DeviceEvent>()
4850

4951
private val logger = MarathonLogging.logger("Scheduler")
5052

@@ -72,7 +74,7 @@ class Scheduler(
7274

7375
private fun subscribeOnDevices(job: Job): Job {
7476
return launch {
75-
for (msg in deviceProvider.subscribe()) {
77+
for (msg in deviceProvider.subscribe(deviceProviderChannel)) {
7678
when (msg) {
7779
is DeviceProvider.DeviceEvent.DeviceConnected -> {
7880
onDeviceConnected(msg, job, coroutineContext)
@@ -117,7 +119,7 @@ class Scheduler(
117119

118120
pools.computeIfAbsent(poolId) { id ->
119121
logger.debug { "pool actor ${id.name} is being created" }
120-
DevicePoolActor(id, configuration, accumulator, analytics, shard, timer, parent, context, testBundleIdentifier)
122+
DevicePoolActor(id, configuration, accumulator, deviceProviderChannel, analytics, shard, timer, parent, context, testBundleIdentifier)
121123
}
122124
pools[poolId]?.send(AddDevice(device)) ?: logger.debug {
123125
"not sending the AddDevice event " +

core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt

+3
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ class DeviceActor(
224224
initializeJob?.cancelAndJoin()
225225
executeJob?.cancelAndJoin()
226226
close()
227+
withContext(NonCancellable) {
228+
pool.send(DevicePoolMessage.FromDevice.DeviceLost(device))
229+
}
227230
}
228231
}
229232
}

vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/AdamDeviceProvider.kt

+36-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.malinskiy.marathon.config.Configuration
1515
import com.malinskiy.marathon.config.vendor.VendorConfiguration
1616
import com.malinskiy.marathon.coroutines.newCoroutineExceptionHandler
1717
import com.malinskiy.marathon.device.DeviceProvider
18+
import com.malinskiy.marathon.device.DeviceProvider.DeviceEvent
1819
import com.malinskiy.marathon.exceptions.NoDevicesException
1920
import com.malinskiy.marathon.log.MarathonLogging
2021
import com.malinskiy.marathon.time.Timer
@@ -52,6 +53,7 @@ class AdamDeviceProvider(
5253
private val logger = MarathonLogging.logger("AdamDeviceProvider")
5354

5455
private val channel: Channel<DeviceProvider.DeviceEvent> = unboundedChannel()
56+
private var scheduler: ReceiveChannel<DeviceProvider.DeviceEvent>? = null
5557

5658
private val dispatcher = newFixedThreadPoolContext(vendorConfiguration.threadingConfiguration.bootWaitingThreads, "DeviceMonitor")
5759
private val installDispatcher = Dispatchers.IO.limitedParallelism(vendorConfiguration.threadingConfiguration.installThreads)
@@ -210,7 +212,40 @@ class AdamDeviceProvider(
210212
socketFactory.close()
211213
}
212214

213-
override fun subscribe() = channel
215+
override fun subscribe(scheduler: ReceiveChannel<DeviceEvent>): ReceiveChannel<DeviceEvent> {
216+
this.scheduler = scheduler
217+
launch {
218+
for (event in scheduler) {
219+
processSchedulerEvents(event)
220+
}
221+
}
222+
return channel
223+
}
224+
225+
private suspend fun processSchedulerEvents(event: DeviceEvent) {
226+
when (event) {
227+
is DeviceEvent.DeviceConnected -> Unit
228+
is DeviceEvent.DeviceDisconnected -> {
229+
if (event.device is AdamAndroidDevice) {
230+
val device = event.device as AdamAndroidDevice
231+
232+
val fullFormSerial = device.adbSerial.contains(":")
233+
var host: String
234+
var port: Int?
235+
if (fullFormSerial) {
236+
host = device.adbSerial.substringBefore(":")
237+
port = device.adbSerial.substringAfter(":").toIntOrNull()
238+
} else {
239+
host = device.adbSerial
240+
port = null
241+
}
242+
device.client.execute(DisconnectDeviceRequest(host, port))
243+
} else {
244+
logger.warn { "Received $event from scheduler, expected device to be AdamAndroidDevice, but was ${event.device::class.java.simpleName}" }
245+
}
246+
}
247+
}
248+
}
214249
}
215250

216251
data class ProvidedDevice(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.malinskiy.marathon.android.adam
2+
3+
import com.malinskiy.adam.extension.readProtocolString
4+
import com.malinskiy.adam.request.ComplexRequest
5+
import com.malinskiy.adam.request.HostTarget
6+
import com.malinskiy.adam.transport.Socket
7+
8+
class DisconnectDeviceRequest(
9+
private val host: String? = null,
10+
private val port: Int? = 5555
11+
) : ComplexRequest<String>(target = HostTarget) {
12+
13+
override fun serialize() = createBaseRequest(
14+
"disconnect:${
15+
if (host == null) {
16+
""
17+
} else if (port != null) {
18+
"$host:$port"
19+
} else {
20+
"$host"
21+
}
22+
}"
23+
)
24+
25+
override suspend fun readElement(socket: Socket) = socket.readProtocolString()
26+
}

vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/AppleSimulatorProvider.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class AppleSimulatorProvider(
105105
private val simulatorFactory = SimulatorFactory(configuration, vendorConfiguration, testBundleIdentifier, gson, track, timer)
106106
private val deviceTracker = DeviceTracker()
107107

108-
override fun subscribe() = channel
108+
override fun subscribe(scheduler: ReceiveChannel<DeviceProvider.DeviceEvent>): ReceiveChannel<DeviceProvider.DeviceEvent> = channel
109109

110110
private val sourceMutex = Mutex()
111111
private lateinit var sourceChannel: ReceiveChannel<Marathondevices>

vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/AppleMacosProvider.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import kotlinx.coroutines.NonCancellable
2828
import kotlinx.coroutines.async
2929
import kotlinx.coroutines.awaitAll
3030
import kotlinx.coroutines.channels.Channel
31+
import kotlinx.coroutines.channels.ReceiveChannel
3132
import kotlinx.coroutines.delay
3233
import kotlinx.coroutines.launch
3334
import kotlinx.coroutines.newFixedThreadPoolContext
@@ -72,7 +73,7 @@ class AppleMacosProvider(
7273
configuration.outputDir
7374
)
7475

75-
override fun subscribe() = channel
76+
override fun subscribe(scheduler: ReceiveChannel<DeviceProvider.DeviceEvent>): ReceiveChannel<DeviceProvider.DeviceEvent> = channel
7677

7778
override suspend fun initialize() = withContext(coroutineContext) {
7879
logger.debug("Initializing AppleMacosProvider")

vendor/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt

+5-6
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ package com.malinskiy.marathon.test
33
import com.malinskiy.marathon.actor.unboundedChannel
44
import com.malinskiy.marathon.device.Device
55
import com.malinskiy.marathon.device.DeviceProvider
6+
import kotlin.coroutines.CoroutineContext
67
import kotlinx.coroutines.CoroutineScope
78
import kotlinx.coroutines.channels.Channel
9+
import kotlinx.coroutines.channels.ReceiveChannel
810
import kotlinx.coroutines.launch
9-
import kotlin.coroutines.CoroutineContext
1011

1112
class StubDeviceProvider : DeviceProvider, CoroutineScope {
1213
lateinit var context: CoroutineContext
1314
lateinit var borrowingDevice: Device
1415

15-
override val coroutineContext: kotlin.coroutines.CoroutineContext
16+
override val coroutineContext: CoroutineContext
1617
get() = context
1718

1819
private val channel: Channel<DeviceProvider.DeviceEvent> = unboundedChannel()
@@ -22,11 +23,9 @@ class StubDeviceProvider : DeviceProvider, CoroutineScope {
2223

2324
override suspend fun borrow() = borrowingDevice
2425

25-
override fun subscribe(): Channel<DeviceProvider.DeviceEvent> {
26+
override fun subscribe(scheduler: ReceiveChannel<DeviceProvider.DeviceEvent>): ReceiveChannel<DeviceProvider.DeviceEvent> {
2627
providingLogic?.let {
27-
launch(context = coroutineContext) {
28-
providingLogic?.invoke(channel)
29-
}
28+
launch(context = coroutineContext) { providingLogic?.invoke(channel) }
3029
}
3130

3231
return channel

0 commit comments

Comments
 (0)