Skip to content

Commit

Permalink
fix: reinstall ios app to clear state reliably (mobile-dev-inc#2118)
Browse files Browse the repository at this point in the history
* fix: reinstall ios app to clear state reliably

* Update maestro-ios-driver/src/main/kotlin/util/LocalSimulatorUtils.kt

Co-authored-by: Igor Lema <48068670+igorsmotto@users.noreply.github.com>

---------

Co-authored-by: Igor Lema <48068670+igorsmotto@users.noreply.github.com>
  • Loading branch information
2 people authored and rasyid7 committed Dec 9, 2024
1 parent 2491aad commit 61fc4eb
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 19 deletions.
133 changes: 115 additions & 18 deletions maestro-ios-driver/src/main/kotlin/util/LocalSimulatorUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import com.fasterxml.jackson.module.kotlin.readValue
import maestro.utils.MaestroTimer
import org.rauschig.jarchivelib.ArchiveFormat
import org.rauschig.jarchivelib.ArchiverFactory
import org.slf4j.LoggerFactory
import util.CommandLineUtils.runCommand
import java.io.File
import java.io.InputStream
import java.lang.ProcessBuilder.Redirect.PIPE
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.createTempDirectory

object LocalSimulatorUtils {
Expand All @@ -17,6 +21,8 @@ object LocalSimulatorUtils {

private val homedir = System.getProperty("user.home")

private val logger = LoggerFactory.getLogger(LocalSimulatorUtils::class.java)

private val allPermissions = listOf(
"calendar",
"camera",
Expand Down Expand Up @@ -171,30 +177,109 @@ object LocalSimulatorUtils {
.waitFor()
}

private fun isAppRunning(deviceId: String, bundleId: String): Boolean {
val process = ProcessBuilder(
listOf(
"xcrun",
"simctl",
"spawn",
deviceId,
"launchctl",
"list",
)
).start()

return String(process.inputStream.readBytes()).trimEnd().contains(bundleId)
}

private fun ensureStopped(deviceId: String, bundleId: String) {
MaestroTimer.withTimeout(10000) {
while (true) {
if (isAppRunning(deviceId, bundleId)) {
Thread.sleep(1000)
} else {
return@withTimeout
}
}
} ?: throw SimctlError("App $bundleId did not stop in time")
}

private fun ensureRunning(deviceId: String, bundleId: String) {
MaestroTimer.withTimeout(10000) {
while (true) {
if (isAppRunning(deviceId, bundleId)) {
return@withTimeout
} else {
Thread.sleep(1000)
}
}
} ?: throw SimctlError("App $bundleId did not start in time")
}

private fun copyDirectoryRecursively(source: Path, target: Path) {
Files.walk(source).forEach { path ->
val targetPath = target.resolve(source.relativize(path).toString())
if (Files.isDirectory(path)) {
Files.createDirectories(targetPath)
} else {
Files.copy(path, targetPath)
}
}
}

private fun deleteFolderRecursively(folder: File): Boolean {
if (folder.isDirectory) {
folder.listFiles()?.forEach { child ->
deleteFolderRecursively(child)
}
}
return folder.delete()
}

private fun reinstallApp(deviceId: String, bundleId: String) {
val pathToBinary = Path(getAppBinaryDirectory(deviceId, bundleId))

if (Files.isDirectory(pathToBinary)) {
val tmpDir = createTempDirectory()
val tmpBundlePath = tmpDir.resolve("$bundleId-${System.currentTimeMillis()}.app")

logger.info("Copying app binary from $pathToBinary to $tmpBundlePath")
Files.copy(pathToBinary, tmpBundlePath)
copyDirectoryRecursively(pathToBinary, tmpBundlePath)

logger.info("Reinstalling and launching $bundleId")
uninstall(deviceId, bundleId)
install(deviceId, tmpBundlePath)
deleteFolderRecursively(tmpBundlePath.toFile())
logger.info("App $bundleId reinstalled and launched")
} else {
throw SimctlError("Could not find app binary for bundle $bundleId at $pathToBinary")
}
}

fun clearAppState(deviceId: String, bundleId: String) {
logger.info("Clearing app $bundleId state")
// Stop the app before clearing the file system
// This prevents the app from saving its state after it has been cleared
terminate(deviceId, bundleId)
ensureStopped(deviceId, bundleId)

// Wait for the app to be stopped
Thread.sleep(1500)

// deletes app data, including container folder
val appDataDirectory = getApplicationDataDirectory(deviceId, bundleId)
ProcessBuilder(listOf("rm", "-rf", appDataDirectory)).start().waitFor()

// forces app container folder to be re-created
val paths = listOf(
"Documents",
"Library",
"Library/Caches",
"Library/Preferences",
"SystemData",
"tmp"
)
// reinstall the app as that is the most stable way to clear state
reinstallApp(deviceId, bundleId)
}

val command = listOf("mkdir", appDataDirectory) + paths.map { "$appDataDirectory/$it" }
ProcessBuilder(command).start().waitFor()
private fun getAppBinaryDirectory(deviceId: String, bundleId: String): String {
val process = ProcessBuilder(
listOf(
"xcrun",
"simctl",
"get_app_container",
deviceId,
bundleId,
)
).start()

return String(process.inputStream.readBytes()).trimEnd()
}

private fun getApplicationDataDirectory(deviceId: String, bundleId: String): String {
Expand Down Expand Up @@ -491,6 +576,18 @@ object LocalSimulatorUtils {
}
}

fun install(deviceId: String, path: Path) {
runCommand(
listOf(
"xcrun",
"simctl",
"install",
deviceId,
path.toAbsolutePath().toString(),
)
)
}

fun install(deviceId: String, stream: InputStream) {
val temp = createTempDirectory()
val extractDir = temp.toFile()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,7 @@ class Orchestra(
maestro.setPermissions(command.appId, permissions)

} catch (e: Exception) {
throw MaestroException.UnableToClearState("Unable to clear state for app ${command.appId}")
throw MaestroException.UnableToClearState("Unable to clear state for app ${command.appId}: ${e.message}")
}

try {
Expand Down

0 comments on commit 61fc4eb

Please sign in to comment.