@@ -5,10 +5,14 @@ import com.fasterxml.jackson.module.kotlin.readValue
5
5
import maestro.utils.MaestroTimer
6
6
import org.rauschig.jarchivelib.ArchiveFormat
7
7
import org.rauschig.jarchivelib.ArchiverFactory
8
+ import org.slf4j.LoggerFactory
8
9
import util.CommandLineUtils.runCommand
9
10
import java.io.File
10
11
import java.io.InputStream
11
12
import java.lang.ProcessBuilder.Redirect.PIPE
13
+ import java.nio.file.Files
14
+ import java.nio.file.Path
15
+ import kotlin.io.path.Path
12
16
import kotlin.io.path.createTempDirectory
13
17
14
18
object LocalSimulatorUtils {
@@ -17,6 +21,8 @@ object LocalSimulatorUtils {
17
21
18
22
private val homedir = System .getProperty(" user.home" )
19
23
24
+ private val logger = LoggerFactory .getLogger(LocalSimulatorUtils ::class .java)
25
+
20
26
private val allPermissions = listOf (
21
27
" calendar" ,
22
28
" camera" ,
@@ -171,30 +177,109 @@ object LocalSimulatorUtils {
171
177
.waitFor()
172
178
}
173
179
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
+
174
260
fun clearAppState (deviceId : String , bundleId : String ) {
261
+ logger.info(" Clearing app $bundleId state" )
175
262
// Stop the app before clearing the file system
176
263
// This prevents the app from saving its state after it has been cleared
177
264
terminate(deviceId, bundleId)
265
+ ensureStopped(deviceId, bundleId)
178
266
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
+ }
195
270
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()
198
283
}
199
284
200
285
private fun getApplicationDataDirectory (deviceId : String , bundleId : String ): String {
@@ -491,6 +576,18 @@ object LocalSimulatorUtils {
491
576
}
492
577
}
493
578
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
+
494
591
fun install (deviceId : String , stream : InputStream ) {
495
592
val temp = createTempDirectory()
496
593
val extractDir = temp.toFile()
0 commit comments