Wait for iOS runtimes to be unmounted

Bug: https://github.com/flutter/flutter/issues/137634
Change-Id: I439d17427c5e25a8ff3dd7112564c1ced186884e
Reviewed-on: https://flutter-review.googlesource.com/c/recipes/+/52041
Reviewed-by: Ricardo Amador <ricardoamador@google.com>
Commit-Queue: Victoria Ashworth <vashworth@google.com>
diff --git a/recipe_modules/osx_sdk/__init__.py b/recipe_modules/osx_sdk/__init__.py
index ae6637d..274ff4f 100644
--- a/recipe_modules/osx_sdk/__init__.py
+++ b/recipe_modules/osx_sdk/__init__.py
@@ -4,6 +4,7 @@
 
 DEPS = [
     'flutter/os_utils',
+    'flutter/retry',
     'recipe_engine/cipd',
     'recipe_engine/context',
     'recipe_engine/file',
diff --git a/recipe_modules/osx_sdk/api.py b/recipe_modules/osx_sdk/api.py
index 23f2329..ef42a10 100644
--- a/recipe_modules/osx_sdk/api.py
+++ b/recipe_modules/osx_sdk/api.py
@@ -460,6 +460,15 @@
             step_text=simulator_cleanup_stderr,
         )
 
+      # Wait until runtimes are unmounted
+      self.m.retry.basic_wrap(
+          self._is_runtimes_unmounted,
+          step_name='Wait for runtimes to unmount',
+          sleep=3.0,
+          backoff_factor=2,
+          max_attempts=3
+      )
+
       if not self._runtime_versions:
         return
 
@@ -471,6 +480,21 @@
             runtime_dmg_cache_dir
         )
 
+  def _is_runtimes_unmounted(self):
+    '''Check if more than one runtime is currently mounted. If more than one
+    is mounted, raise a `StepFailure`.
+    '''
+    runtime_simulators = self.m.step(
+        'list runtimes', ['xcrun', 'simctl', 'list', 'runtimes'],
+        stdout=self.m.raw_io.output_text(add_output_log=True)
+    ).stdout.splitlines()
+
+    # There should not be more than one runtime after deleting all. There may
+    # be one if the runtime is included within Xcode, which means it won't be
+    # unmounted.
+    if len(runtime_simulators[1:]) > 1:
+      raise self.m.step.StepFailure('Runtimes not unmounted yet')
+
   def _is_runtime_mounted(
       self, runtime_version, xcode_version, runtime_simulators
   ):
diff --git a/recipe_modules/osx_sdk/examples/full.expected/mac_13_cleanup_no_runtimes.json b/recipe_modules/osx_sdk/examples/full.expected/mac_13_cleanup_no_runtimes.json
index 8374f79..38ccc27 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/mac_13_cleanup_no_runtimes.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/mac_13_cleanup_no_runtimes.json
@@ -87,6 +87,20 @@
     ]
   },
   {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache.list runtimes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "install runtimes"
   },
@@ -236,6 +250,20 @@
     ]
   },
   {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache (2).list runtimes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "install runtimes (2)"
   },
@@ -404,6 +432,20 @@
     ]
   },
   {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache (3).list runtimes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "install runtimes (3)"
   },
@@ -557,6 +599,20 @@
     ]
   },
   {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache (4).list runtimes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "install runtimes (4)"
   },
diff --git a/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_already_mounted.json b/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_already_mounted.json
index b7d655f..8f61842 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_already_mounted.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_already_mounted.json
@@ -461,6 +461,20 @@
   },
   {
     "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache.list runtimes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
+    "cmd": [
       "vpython3",
       "-u",
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
diff --git a/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_clean.json b/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_clean.json
index 95ebb4f..5035057 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_clean.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_clean.json
@@ -89,6 +89,37 @@
   },
   {
     "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache.list runtimes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@== Runtimes ==@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@iOS 16.4 (16.2 - 20E247) - com.apple.CoreSimulator.SimRuntime.iOS-16-4@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@iOS 16.2 (16.2 - 20C52) - com.apple.CoreSimulator.SimRuntime.iOS-16-2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache.list runtimes (2)",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
+    "cmd": [
       "vpython3",
       "-u",
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
@@ -394,6 +425,20 @@
   },
   {
     "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache (2).list runtimes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
+    "cmd": [
       "vpython3",
       "-u",
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
@@ -539,6 +584,20 @@
   },
   {
     "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache (3).list runtimes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
+    "cmd": [
       "vpython3",
       "-u",
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
diff --git a/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_not_mounted.json b/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_not_mounted.json
index ca4f3ad..598f9b0 100644
--- a/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_not_mounted.json
+++ b/recipe_modules/osx_sdk/examples/full.expected/mac_13_explicit_runtime_version_not_mounted.json
@@ -474,6 +474,20 @@
   },
   {
     "cmd": [
+      "xcrun",
+      "simctl",
+      "list",
+      "runtimes"
+    ],
+    "infra_step": true,
+    "name": "Cleaning up runtimes cache.list runtimes",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
+    "cmd": [
       "vpython3",
       "-u",
       "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
diff --git a/recipe_modules/osx_sdk/examples/full.py b/recipe_modules/osx_sdk/examples/full.py
index ed9b2fc..900ff30 100644
--- a/recipe_modules/osx_sdk/examples/full.py
+++ b/recipe_modules/osx_sdk/examples/full.py
@@ -5,6 +5,7 @@
 DEPS = [
     'flutter/os_utils',
     'flutter/osx_sdk',
+    'flutter/retry',
     'recipe_engine/file',
     'recipe_engine/json',
     'recipe_engine/path',
@@ -286,6 +287,15 @@
           stderr=api.raw_io.output_text('No matching images found to delete')
       ),
       api.step_data(
+          'Cleaning up runtimes cache.list runtimes',
+          stdout=api.raw_io.output_text(
+              '== Runtimes ==\n' +
+              'iOS 16.4 (16.2 - 20E247) - com.apple.CoreSimulator.SimRuntime.iOS-16-4\n'
+              +
+              'iOS 16.2 (16.2 - 20C52) - com.apple.CoreSimulator.SimRuntime.iOS-16-2'
+          )
+      ),
+      api.step_data(
           'install runtimes.cipd describe ios-16-4_14e300c.cipd describe infra_internal/ios/xcode/ios_runtime_dmg',
           api.json.output({
               'result': {