Skip to main content
Glama
MaestroSessionManager.kt15.4 kB
/* * * Copyright (c) 2022 mobile.dev inc. * * 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 * * http://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 maestro.cli.session import dadb.Dadb import dadb.adbserver.AdbServer import ios.LocalIOSDevice import ios.devicectl.DeviceControlIOSDevice import device.SimctlIOSDevice import ios.xctest.XCTestIOSDevice import maestro.Maestro import maestro.device.Device import maestro.cli.device.PickDeviceInteractor import maestro.cli.driver.DriverBuilder import maestro.cli.driver.RealIOSDeviceDriver import maestro.cli.util.PrintUtils import maestro.device.Platform import maestro.utils.CliInsights import maestro.cli.util.ScreenReporter import maestro.drivers.AndroidDriver import maestro.drivers.IOSDriver import maestro.orchestra.WorkspaceConfig.PlatformConfiguration import maestro.orchestra.workspace.WorkspaceExecutionPlanner import org.slf4j.LoggerFactory import util.IOSDeviceType import util.XCRunnerCLIUtils import xcuitest.XCTestClient import xcuitest.XCTestDriverClient import xcuitest.installer.Context import xcuitest.installer.LocalXCTestInstaller import xcuitest.installer.LocalXCTestInstaller.* import java.nio.file.Paths import java.util.UUID import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlin.io.path.pathString object MaestroSessionManager { private const val defaultHost = "localhost" private const val defaultXctestHost = "127.0.0.1" private const val defaultXcTestPort = 22087 private val executor = Executors.newScheduledThreadPool(1) private val logger = LoggerFactory.getLogger(MaestroSessionManager::class.java) fun <T> newSession( host: String?, port: Int?, driverHostPort: Int?, deviceId: String?, teamId: String? = null, platform: String? = null, isStudio: Boolean = false, isHeadless: Boolean = false, reinstallDriver: Boolean = true, deviceIndex: Int? = null, executionPlan: WorkspaceExecutionPlanner.ExecutionPlan? = null, block: (MaestroSession) -> T, ): T { val selectedDevice = selectDevice( host = host, port = port, driverHostPort = driverHostPort, deviceId = deviceId, teamId = teamId, platform = Platform.fromString(platform), deviceIndex = deviceIndex, ) val sessionId = UUID.randomUUID().toString() val heartbeatFuture = executor.scheduleAtFixedRate( { try { Thread.sleep(1000) // Add a 1-second delay here for fixing race condition SessionStore.heartbeat(sessionId, selectedDevice.platform) } catch (e: Exception) { logger.error("Failed to record heartbeat", e) } }, 0L, 5L, TimeUnit.SECONDS ) val session = createMaestro( selectedDevice = selectedDevice, connectToExistingSession = if (isStudio) { false } else { SessionStore.hasActiveSessions( sessionId, selectedDevice.platform ) }, isStudio = isStudio, isHeadless = isHeadless, driverHostPort = driverHostPort, reinstallDriver = reinstallDriver, platformConfiguration = executionPlan?.workspaceConfig?.platform ) Runtime.getRuntime().addShutdownHook(thread(start = false) { heartbeatFuture.cancel(true) SessionStore.delete(sessionId, selectedDevice.platform) runCatching { ScreenReporter.reportMaxDepth() } if (SessionStore.activeSessions().isEmpty()) { session.close() } }) return block(session) } private fun selectDevice( host: String?, port: Int?, driverHostPort: Int?, deviceId: String?, platform: Platform? = null, teamId: String? = null, deviceIndex: Int? = null, ): SelectedDevice { if (deviceId == "chromium" || platform == Platform.WEB) { return SelectedDevice( platform = Platform.WEB, deviceType = Device.DeviceType.BROWSER ) } if (host == null) { val device = PickDeviceInteractor.pickDevice(deviceId, driverHostPort, platform, deviceIndex) if (device.deviceType == Device.DeviceType.REAL && device.platform == Platform.IOS) { PrintUtils.message("Detected connected iPhone with ${device.instanceId}!") val driverBuilder = DriverBuilder() RealIOSDeviceDriver( destination = "platform=iOS,id=${device.instanceId}", teamId = teamId, driverBuilder = driverBuilder ).validateAndUpdateDriver() } return SelectedDevice( platform = device.platform, device = device, deviceType = device.deviceType ) } if (isAndroid(host, port)) { val deviceType = when { deviceId?.startsWith("emulator") == true -> Device.DeviceType.EMULATOR else -> Device.DeviceType.REAL } return SelectedDevice( platform = Platform.ANDROID, host = host, port = port, deviceId = deviceId, deviceType = deviceType ) } return SelectedDevice( platform = Platform.IOS, host = null, port = null, deviceId = deviceId, deviceType = Device.DeviceType.SIMULATOR ) } private fun createMaestro( selectedDevice: SelectedDevice, connectToExistingSession: Boolean, isStudio: Boolean, isHeadless: Boolean, reinstallDriver: Boolean, driverHostPort: Int?, platformConfiguration: PlatformConfiguration? = null, ): MaestroSession { return when { selectedDevice.device != null -> MaestroSession( maestro = when (selectedDevice.device.platform) { Platform.ANDROID -> createAndroid( selectedDevice.device.instanceId, !connectToExistingSession, driverHostPort, ) Platform.IOS -> createIOS( selectedDevice.device.instanceId, !connectToExistingSession, driverHostPort, reinstallDriver, deviceType = selectedDevice.device.deviceType, platformConfiguration = platformConfiguration ) Platform.WEB -> pickWebDevice(isStudio, isHeadless) }, device = selectedDevice.device, ) selectedDevice.platform == Platform.ANDROID -> MaestroSession( maestro = pickAndroidDevice( selectedDevice.host, selectedDevice.port, driverHostPort, !connectToExistingSession, ), device = null, ) selectedDevice.platform == Platform.IOS -> MaestroSession( maestro = pickIOSDevice( deviceId = selectedDevice.deviceId, openDriver = !connectToExistingSession, driverHostPort = driverHostPort ?: defaultXcTestPort, reinstallDriver = reinstallDriver, platformConfiguration = platformConfiguration, ), device = null, ) selectedDevice.platform == Platform.WEB -> MaestroSession( maestro = pickWebDevice(isStudio, isHeadless), device = null ) else -> error("Unable to create Maestro session") } } private fun isAndroid(host: String?, port: Int?): Boolean { return try { val dadb = if (port != null) { Dadb.create(host ?: defaultHost, port) } else { Dadb.discover(host ?: defaultHost) ?: createAdbServerDadb() ?: error("No android devices found.") } dadb.close() true } catch (_: Exception) { false } } private fun pickAndroidDevice( host: String?, port: Int?, driverHostPort: Int?, openDriver: Boolean, ): Maestro { val dadb = if (port != null) { Dadb.create(host ?: defaultHost, port) } else { Dadb.discover(host ?: defaultHost) ?: createAdbServerDadb() ?: error("No android devices found.") } return Maestro.android( driver = AndroidDriver(dadb, driverHostPort), openDriver = openDriver, ) } private fun createAdbServerDadb(): Dadb? { return try { AdbServer.createDadb(adbServerPort = 5038) } catch (ignored: Exception) { null } } private fun pickIOSDevice( deviceId: String?, openDriver: Boolean, driverHostPort: Int, reinstallDriver: Boolean, platformConfiguration: PlatformConfiguration?, ): Maestro { val device = PickDeviceInteractor.pickDevice(deviceId, driverHostPort) return createIOS( device.instanceId, openDriver, driverHostPort, reinstallDriver, deviceType = device.deviceType, platformConfiguration = platformConfiguration ) } private fun createAndroid( instanceId: String, openDriver: Boolean, driverHostPort: Int?, ): Maestro { val driver = AndroidDriver( dadb = Dadb .list() .find { it.toString() == instanceId } ?: Dadb.discover() ?: error("Unable to find device with id $instanceId"), hostPort = driverHostPort, emulatorName = instanceId, ) return Maestro.android( driver = driver, openDriver = openDriver, ) } private fun createIOS( deviceId: String, openDriver: Boolean, driverHostPort: Int?, reinstallDriver: Boolean, platformConfiguration: PlatformConfiguration?, deviceType: Device.DeviceType, ): Maestro { val iOSDeviceType = when (deviceType) { Device.DeviceType.REAL -> IOSDeviceType.REAL Device.DeviceType.SIMULATOR -> IOSDeviceType.SIMULATOR else -> { throw UnsupportedOperationException("Unsupported device type $deviceType for iOS platform") } } val iOSDriverConfig = when (deviceType) { Device.DeviceType.REAL -> { val maestroDirectory = Paths.get(System.getProperty("user.home"), ".maestro") val driverPath = maestroDirectory.resolve("maestro-iphoneos-driver-build").resolve("driver-iphoneos") .resolve("Build").resolve("Products") IOSDriverConfig( prebuiltRunner = false, sourceDirectory = driverPath.pathString, context = Context.CLI, snapshotKeyHonorModalViews = platformConfiguration?.ios?.snapshotKeyHonorModalViews ) } Device.DeviceType.SIMULATOR -> { IOSDriverConfig( prebuiltRunner = false, sourceDirectory = "driver-iPhoneSimulator", context = Context.CLI, snapshotKeyHonorModalViews = platformConfiguration?.ios?.snapshotKeyHonorModalViews ) } else -> throw UnsupportedOperationException("Unsupported device type $deviceType for iOS platform") } val deviceController = when (deviceType) { Device.DeviceType.REAL -> { val device = util.LocalIOSDevice().listDeviceViaDeviceCtl(deviceId) val deviceCtlDevice = DeviceControlIOSDevice(deviceId = device.identifier) deviceCtlDevice } Device.DeviceType.SIMULATOR -> { val simctlIOSDevice = SimctlIOSDevice( deviceId = deviceId, ) simctlIOSDevice } else -> throw UnsupportedOperationException("Unsupported device type $deviceType for iOS platform") } val xcTestInstaller = LocalXCTestInstaller( deviceId = deviceId, host = defaultXctestHost, defaultPort = driverHostPort ?: defaultXcTestPort, reinstallDriver = reinstallDriver, deviceType = iOSDeviceType, iOSDriverConfig = iOSDriverConfig, deviceController = deviceController ) val xcTestDriverClient = XCTestDriverClient( installer = xcTestInstaller, client = XCTestClient(defaultXctestHost, driverHostPort ?: defaultXcTestPort), reinstallDriver = reinstallDriver, ) val xcTestDevice = XCTestIOSDevice( deviceId = deviceId, client = xcTestDriverClient, getInstalledApps = { XCRunnerCLIUtils.listApps(deviceId) }, ) val iosDriver = IOSDriver( LocalIOSDevice( deviceId = deviceId, xcTestDevice = xcTestDevice, deviceController = deviceController, insights = CliInsights ), insights = CliInsights ) return Maestro.ios( driver = iosDriver, openDriver = openDriver || xcTestDevice.isShutdown(), ) } private fun pickWebDevice(isStudio: Boolean, isHeadless: Boolean): Maestro { return Maestro.web(isStudio, isHeadless) } private data class SelectedDevice( val platform: Platform, val device: Device.Connected? = null, val host: String? = null, val port: Int? = null, val deviceId: String? = null, val deviceType: Device.DeviceType, ) data class MaestroSession( val maestro: Maestro, val device: Device? = null, ) { fun close() { maestro.close() } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mobile-dev-inc/Maestro'

If you have feedback or need assistance with the MCP directory API, please join our Discord server