Skip to content

Commit 11b8f7a

Browse files
fix: reinstall ios app to clear state reliably (#2118)
* fix: reinstall ios app to clear state reliably * Update maestro-ios-driver/src/main/kotlin/util/LocalSimulatorUtils.kt Co-authored-by: Igor Lema <[email protected]> --------- Co-authored-by: Igor Lema <[email protected]>
1 parent e957c5e commit 11b8f7a

File tree

2 files changed

+116
-19
lines changed

2 files changed

+116
-19
lines changed

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

+115-18
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import com.fasterxml.jackson.module.kotlin.readValue
55
import maestro.utils.MaestroTimer
66
import org.rauschig.jarchivelib.ArchiveFormat
77
import org.rauschig.jarchivelib.ArchiverFactory
8+
import org.slf4j.LoggerFactory
89
import util.CommandLineUtils.runCommand
910
import java.io.File
1011
import java.io.InputStream
1112
import java.lang.ProcessBuilder.Redirect.PIPE
13+
import java.nio.file.Files
14+
import java.nio.file.Path
15+
import kotlin.io.path.Path
1216
import kotlin.io.path.createTempDirectory
1317

1418
object LocalSimulatorUtils {
@@ -17,6 +21,8 @@ object LocalSimulatorUtils {
1721

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

24+
private val logger = LoggerFactory.getLogger(LocalSimulatorUtils::class.java)
25+
2026
private val allPermissions = listOf(
2127
"calendar",
2228
"camera",
@@ -171,30 +177,109 @@ object LocalSimulatorUtils {
171177
.waitFor()
172178
}
173179

180+
private fun isAppRunning(deviceId: String, bundleId: String): Boolean {
181+
val process = ProcessBuilder(
182+
listOf(
183+
"xcrun",
184+
"simctl",
185+
"spawn",
186+
deviceId,
187+
"launchctl",
188+
"list",
189+
)
190+
).start()
191+
192+
return String(process.inputStream.readBytes()).trimEnd().contains(bundleId)
193+
}
194+
195+
private fun ensureStopped(deviceId: String, bundleId: String) {
196+
MaestroTimer.withTimeout(10000) {
197+
while (true) {
198+
if (isAppRunning(deviceId, bundleId)) {
199+
Thread.sleep(1000)
200+
} else {
201+
return@withTimeout
202+
}
203+
}
204+
} ?: throw SimctlError("App $bundleId did not stop in time")
205+
}
206+
207+
private fun ensureRunning(deviceId: String, bundleId: String) {
208+
MaestroTimer.withTimeout(10000) {
209+
while (true) {
210+
if (isAppRunning(deviceId, bundleId)) {
211+
return@withTimeout
212+
} else {
213+
Thread.sleep(1000)
214+
}
215+
}
216+
} ?: throw SimctlError("App $bundleId did not start in time")
217+
}
218+
219+
private fun copyDirectoryRecursively(source: Path, target: Path) {
220+
Files.walk(source).forEach { path ->
221+
val targetPath = target.resolve(source.relativize(path).toString())
222+
if (Files.isDirectory(path)) {
223+
Files.createDirectories(targetPath)
224+
} else {
225+
Files.copy(path, targetPath)
226+
}
227+
}
228+
}
229+
230+
private fun deleteFolderRecursively(folder: File): Boolean {
231+
if (folder.isDirectory) {
232+
folder.listFiles()?.forEach { child ->
233+
deleteFolderRecursively(child)
234+
}
235+
}
236+
return folder.delete()
237+
}
238+
239+
private fun reinstallApp(deviceId: String, bundleId: String) {
240+
val pathToBinary = Path(getAppBinaryDirectory(deviceId, bundleId))
241+
242+
if (Files.isDirectory(pathToBinary)) {
243+
val tmpDir = createTempDirectory()
244+
val tmpBundlePath = tmpDir.resolve("$bundleId-${System.currentTimeMillis()}.app")
245+
246+
logger.info("Copying app binary from $pathToBinary to $tmpBundlePath")
247+
Files.copy(pathToBinary, tmpBundlePath)
248+
copyDirectoryRecursively(pathToBinary, tmpBundlePath)
249+
250+
logger.info("Reinstalling and launching $bundleId")
251+
uninstall(deviceId, bundleId)
252+
install(deviceId, tmpBundlePath)
253+
deleteFolderRecursively(tmpBundlePath.toFile())
254+
logger.info("App $bundleId reinstalled and launched")
255+
} else {
256+
throw SimctlError("Could not find app binary for bundle $bundleId at $pathToBinary")
257+
}
258+
}
259+
174260
fun clearAppState(deviceId: String, bundleId: String) {
261+
logger.info("Clearing app $bundleId state")
175262
// Stop the app before clearing the file system
176263
// This prevents the app from saving its state after it has been cleared
177264
terminate(deviceId, bundleId)
265+
ensureStopped(deviceId, bundleId)
178266

179-
// Wait for the app to be stopped
180-
Thread.sleep(1500)
181-
182-
// deletes app data, including container folder
183-
val appDataDirectory = getApplicationDataDirectory(deviceId, bundleId)
184-
ProcessBuilder(listOf("rm", "-rf", appDataDirectory)).start().waitFor()
185-
186-
// forces app container folder to be re-created
187-
val paths = listOf(
188-
"Documents",
189-
"Library",
190-
"Library/Caches",
191-
"Library/Preferences",
192-
"SystemData",
193-
"tmp"
194-
)
267+
// reinstall the app as that is the most stable way to clear state
268+
reinstallApp(deviceId, bundleId)
269+
}
195270

196-
val command = listOf("mkdir", appDataDirectory) + paths.map { "$appDataDirectory/$it" }
197-
ProcessBuilder(command).start().waitFor()
271+
private fun getAppBinaryDirectory(deviceId: String, bundleId: String): String {
272+
val process = ProcessBuilder(
273+
listOf(
274+
"xcrun",
275+
"simctl",
276+
"get_app_container",
277+
deviceId,
278+
bundleId,
279+
)
280+
).start()
281+
282+
return String(process.inputStream.readBytes()).trimEnd()
198283
}
199284

200285
private fun getApplicationDataDirectory(deviceId: String, bundleId: String): String {
@@ -491,6 +576,18 @@ object LocalSimulatorUtils {
491576
}
492577
}
493578

579+
fun install(deviceId: String, path: Path) {
580+
runCommand(
581+
listOf(
582+
"xcrun",
583+
"simctl",
584+
"install",
585+
deviceId,
586+
path.toAbsolutePath().toString(),
587+
)
588+
)
589+
}
590+
494591
fun install(deviceId: String, stream: InputStream) {
495592
val temp = createTempDirectory()
496593
val extractDir = temp.toFile()

maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ class Orchestra(
804804
maestro.setPermissions(command.appId, permissions)
805805

806806
} catch (e: Exception) {
807-
throw MaestroException.UnableToClearState("Unable to clear state for app ${command.appId}")
807+
throw MaestroException.UnableToClearState("Unable to clear state for app ${command.appId}: ${e.message}")
808808
}
809809

810810
try {

0 commit comments

Comments
 (0)