Retry steps of granting Xcode automation permission on failure

Bug: https://github.com/flutter/flutter/issues/139693
Change-Id: I4b41fa2ebcb82c6b4756f20bab93020d6bfe8400
Reviewed-on: https://flutter-review.googlesource.com/c/recipes/+/53040
Commit-Queue: Victoria Ashworth <vashworth@google.com>
Reviewed-by: Keyong Han <keyonghan@google.com>
diff --git a/recipe_modules/os_utils/__init__.py b/recipe_modules/os_utils/__init__.py
index 1df74ac..a164017 100644
--- a/recipe_modules/os_utils/__init__.py
+++ b/recipe_modules/os_utils/__init__.py
@@ -1,5 +1,6 @@
 DEPS = [
     'flutter/repo_util',
+    'flutter/retry',
     'recipe_engine/context',
     'recipe_engine/file',
     'recipe_engine/path',
@@ -8,4 +9,5 @@
     'recipe_engine/raw_io',
     'recipe_engine/step',
     'recipe_engine/swarming',
+    "recipe_engine/time",
 ]
diff --git a/recipe_modules/os_utils/api.py b/recipe_modules/os_utils/api.py
index 87e5a9c..7e545b9 100644
--- a/recipe_modules/os_utils/api.py
+++ b/recipe_modules/os_utils/api.py
@@ -351,7 +351,8 @@
             cmd = [resource_name, device_id]
             self.m.step('Run app to dismiss dialogs', cmd)
         with self.m.step.nest('Dismiss Xcode automation dialogs'):
-          self._dismiss_xcode_automation_dialogs(device_id)
+          with self.m.context(infra_steps=True):
+            self._dismiss_xcode_automation_dialogs(device_id)
 
   def _dismiss_xcode_automation_dialogs(self, device_id):
     """Dismiss Xcode automation permission dialog and update permission db.
@@ -374,7 +375,6 @@
             'devices',
             '-v',
         ],
-        infra_step=True,
         raise_on_failure=False,
         ok_ret='any',
     )
@@ -391,7 +391,6 @@
             '-v',
         ],
         stdout=self.m.raw_io.output_text(),
-        infra_step=True,
         raise_on_failure=False,
         ok_ret='any',
     ).stdout.rstrip()
@@ -410,17 +409,10 @@
     tcc_directory_path, db_path, backup_db_path = self._get_tcc_path()
 
     # Ensure db exists
-    self.m.step(
-        'List TCC directory',
-        ['ls', tcc_directory_path],
-        infra_step=True,
-    )
-
     files = self.m.step(
         'Find TCC directory',
         ['ls', tcc_directory_path],
-        stdout=self.m.raw_io.output_text(),
-        infra_step=True,
+        stdout=self.m.raw_io.output_text(add_output_log=True),
     ).stdout.rstrip()
 
     if XCODE_AUTOMATION_DB not in files:
@@ -430,7 +422,7 @@
       )
 
     # Print contents of db for potential debugging purposes.
-    self._query_automation_db_step(db_path)
+    self._query_automation_db_step_with_retry(db_path)
 
     # Create backup db if there isn't one.
     # If there is already a backup, it's most likely that a previous run did
@@ -439,13 +431,55 @@
       self.m.step(
           'Create backup db',
           ['cp', db_path, backup_db_path],
-          infra_step=True,
       )
 
+    self.m.retry.basic_wrap(
+        lambda timeout: self._trigger_automation_permission(
+            db_path,
+            timeout=timeout,
+        ),
+        step_name='Wait to add entry in TCC db',
+        sleep=2.0,
+        backoff_factor=2,
+        max_attempts=3,
+        timeout=2,
+    )
+
+    # Update TCC.db. If fails, try up to 3 times. It may fail if the db is locked.
+    self.m.retry.basic_wrap(
+        lambda timeout: self._update_automation_permission_db(
+            db_path,
+            timeout=timeout,
+        ),
+        step_name='Wait to update TCC db',
+        sleep=2.0,
+        backoff_factor=2,
+        max_attempts=3
+    )
+
+    # Print contents of db for potential debugging purposes.
+    self._query_automation_db_step_with_retry(db_path)
+
+    # Xcode was opened by Applescript, so kill it.
+    self.m.step(
+        'Kill Xcode',
+        ['killall', '-9', 'Xcode'],
+        ok_ret='any',
+    )
+
+  def _trigger_automation_permission(self, db_path, timeout=2):
+    """Trigger Xcode automation dialog to appear and then kill the dialog.
+    Killing the dialog will add an entry for the permission to the TCC.db.
+    Raises an error if dialog fails to add entry to db.
+
+    Args:
+      db_path(string): A string of the path to the sqlite database.
+    """
+
     # Run an arbitrary AppleScript Xcode command to trigger permissions dialog.
     # The AppleScript counts how many Xcode windows are open.
     # The script will hang if permission has not been given, so timeout after
-    # a few seconds.
+    # a few seconds. For each attempt, use a longer timeout.
     self.m.step(
         'Trigger dialog',
         [
@@ -459,10 +493,9 @@
             '-e',
             'end tell',
         ],
-        infra_step=True,
         raise_on_failure=False,
         ok_ret='any',
-        timeout=2,
+        timeout=timeout,
     )
 
     # Kill the dialog. After killing the dialog, an entry for the app requesting
@@ -470,33 +503,11 @@
     self.m.step(
         'Dismiss dialog',
         ['killall', '-9', 'UserNotificationCenter'],
-        infra_step=True,
         ok_ret='any',
     )
 
-    # Print contents of db for potential debugging purposes.
-    self._query_automation_db_step(db_path)
-
-    # Update the db to make it think permission was given.
-    self.m.step(
-        'Update db',
-        [
-            'sqlite3', db_path,
-            "UPDATE access SET auth_value = 2, auth_reason = 3, flags = NULL WHERE service = 'kTCCServiceAppleEvents' AND indirect_object_identifier = 'com.apple.dt.Xcode'"
-        ],
-        infra_step=True,
-    )
-
-    # Print contents of db for potential debugging purposes.
-    self._query_automation_db_step(db_path)
-
-    # Xcode was opened by Applescript, so kill it.
-    self.m.step(
-        'Kill Xcode',
-        ['killall', '-9', 'Xcode'],
-        infra_step=True,
-        ok_ret='any',
-    )
+    if 'Xcode' not in self._query_automation_db_step_with_retry(db_path):
+      raise self.m.step.InfraFailure('Xcode entry not found in TCC.db')
 
   def _get_tcc_path(self):
     """Constructs paths to the TCC directory, TCC db, and TCC backup db.
@@ -516,21 +527,59 @@
     )
     return tcc_directory_path, db_path, backup_db_path
 
-  def _query_automation_db_step(self, db_path):
+  # pylint: disable=unused-argument
+  def _update_automation_permission_db(self, db_path, timeout=None):
+    self.m.step(
+        'Update db',
+        [
+            'sqlite3', db_path,
+            "UPDATE access SET auth_value = 2, auth_reason = 3, flags = NULL WHERE service = 'kTCCServiceAppleEvents' AND indirect_object_identifier = 'com.apple.dt.Xcode'"
+        ],
+    )
+
+  def _query_automation_db_step_with_retry(self, db_path):
+    """Queries the TCC database with 3 retries. Sometimes if the database is
+    locked, query will fail. So wait and try again.
+
+    Args:
+      db_path(string): A string of the path to the sqlite database.
+
+    Returns:
+      A string of the query's output.
+    """
+
+    return self.m.retry.basic_wrap(
+        lambda timeout: self._query_automation_db_step(
+            db_path,
+            timeout=timeout,
+        ),
+        step_name='Wait to query TCC db',
+        sleep=2.0,
+        backoff_factor=1,
+        max_attempts=3
+    )
+
+  # pylint: disable=unused-argument
+  def _query_automation_db_step(self, db_path, timeout=None):
     """Queries the TCC database.
 
     Args:
       db_path(string): A string of the path to the sqlite database.
+
+    Returns:
+      A string of the query's output.
     """
-    self.m.step(
+    query_results = self.m.step(
         'Query TCC db',
         [
             'sqlite3', db_path,
             'SELECT service, client, client_type, auth_value, auth_reason, indirect_object_identifier_type, indirect_object_identifier, flags, last_modified FROM access WHERE service = "kTCCServiceAppleEvents"'
         ],
-        infra_step=True,
+        stdout=self.m.raw_io.output_text(add_output_log=True),
     )
 
+    return query_results.stdout.rstrip()
+
   def reset_automation_dialogs(self):
     """Reset Xcode Automation permissions."""
     if str(self.m.swarming.bot_id
@@ -560,7 +609,7 @@
         )
 
         # Print contents of db for potential debugging purposes.
-        self._query_automation_db_step(db_path)
+        self._query_automation_db_step_with_retry(db_path)
 
   def _checkout_cocoon(self):
     """Checkout cocoon at HEAD to the cache and return the path."""
diff --git a/recipe_modules/os_utils/examples/full.expected/clean_derived_data.json b/recipe_modules/os_utils/examples/full.expected/clean_derived_data.json
index 9112833..3592f1e 100644
--- a/recipe_modules/os_utils/examples/full.expected/clean_derived_data.json
+++ b/recipe_modules/os_utils/examples/full.expected/clean_derived_data.json
@@ -521,20 +521,11 @@
       "Users/fakeuser/Library/Application Support/com.apple.TCC"
     ],
     "infra_step": true,
-    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.List TCC directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "ls",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC"
-    ],
-    "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@TCC.db@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -546,7 +537,8 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -601,7 +593,9 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (2)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@service|client|client_type|auth_value|auth_reason|auth_version|com.apple.dt.Xcode|flags|last_modified@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -625,7 +619,8 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (3)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
diff --git a/recipe_modules/os_utils/examples/full.expected/dimiss_dialog_xcode_automation_fails_find_db.json b/recipe_modules/os_utils/examples/full.expected/dimiss_dialog_xcode_automation_fails_find_db.json
index 17de95f..d1a8025 100644
--- a/recipe_modules/os_utils/examples/full.expected/dimiss_dialog_xcode_automation_fails_find_db.json
+++ b/recipe_modules/os_utils/examples/full.expected/dimiss_dialog_xcode_automation_fails_find_db.json
@@ -525,20 +525,10 @@
       "Users/fakeuser/Library/Application Support/com.apple.TCC"
     ],
     "infra_step": true,
-    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.List TCC directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "ls",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC"
-    ],
-    "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
diff --git a/recipe_modules/os_utils/examples/full.expected/reset_dialog_xcode_automation_fails_find_db.json b/recipe_modules/os_utils/examples/full.expected/dimiss_dialog_xcode_automation_fails_update_db.json
similarity index 87%
copy from recipe_modules/os_utils/examples/full.expected/reset_dialog_xcode_automation_fails_find_db.json
copy to recipe_modules/os_utils/examples/full.expected/dimiss_dialog_xcode_automation_fails_update_db.json
index 9308ce2..e6975c1 100644
--- a/recipe_modules/os_utils/examples/full.expected/reset_dialog_xcode_automation_fails_find_db.json
+++ b/recipe_modules/os_utils/examples/full.expected/dimiss_dialog_xcode_automation_fails_update_db.json
@@ -312,7 +312,10 @@
   },
   {
     "cmd": [],
-    "name": "Dismiss dialogs"
+    "name": "Dismiss dialogs",
+    "~followup_annotations": [
+      "@@@STEP_EXCEPTION@@@"
+    ]
   },
   {
     "cmd": [],
@@ -475,7 +478,8 @@
     "cmd": [],
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_EXCEPTION@@@"
     ]
   },
   {
@@ -521,20 +525,11 @@
       "Users/fakeuser/Library/Application Support/com.apple.TCC"
     ],
     "infra_step": true,
-    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.List TCC directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "ls",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC"
-    ],
-    "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@TCC.db@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -546,7 +541,8 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -601,17 +597,37 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (2)",
     "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "osascript",
+      "-e",
+      "tell app \"Xcode\"",
+      "-e",
+      "launch",
+      "-e",
+      "count window",
+      "-e",
+      "end tell"
+    ],
+    "infra_step": true,
+    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Trigger dialog (2)",
+    "timeout": 4,
+    "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@2@@@"
     ]
   },
   {
     "cmd": [
-      "sqlite3",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC/TCC.db",
-      "UPDATE access SET auth_value = 2, auth_reason = 3, flags = NULL WHERE service = 'kTCCServiceAppleEvents' AND indirect_object_identifier = 'com.apple.dt.Xcode'"
+      "killall",
+      "-9",
+      "UserNotificationCenter"
     ],
     "infra_step": true,
-    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Update db",
+    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Dismiss dialog (2)",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@2@@@"
     ]
@@ -625,6 +641,26 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (3)",
     "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "osascript",
+      "-e",
+      "tell app \"Xcode\"",
+      "-e",
+      "launch",
+      "-e",
+      "count window",
+      "-e",
+      "end tell"
+    ],
+    "infra_step": true,
+    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Trigger dialog (3)",
+    "timeout": 8,
+    "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@2@@@"
     ]
   },
@@ -632,82 +668,31 @@
     "cmd": [
       "killall",
       "-9",
-      "Xcode"
+      "UserNotificationCenter"
     ],
     "infra_step": true,
-    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Kill Xcode",
+    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Dismiss dialog (3)",
     "~followup_annotations": [
       "@@@STEP_NEST_LEVEL@2@@@"
     ]
   },
   {
-    "cmd": [],
-    "name": "Reset Xcode automation dialogs"
-  },
-  {
-    "cmd": [
-      "ls",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC"
-    ],
-    "infra_step": true,
-    "name": "Reset Xcode automation dialogs.Find TCC directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "cp",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC/TCC.db.backup",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC/TCC.db"
-    ],
-    "name": "Reset Xcode automation dialogs.Restore from backup db",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "rm",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC/TCC.db.backup"
-    ],
-    "name": "Reset Xcode automation dialogs.Remove backup",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
-    ]
-  },
-  {
     "cmd": [
       "sqlite3",
       "Users/fakeuser/Library/Application Support/com.apple.TCC/TCC.db",
       "SELECT service, client, client_type, auth_value, auth_reason, indirect_object_identifier_type, indirect_object_identifier, flags, last_modified FROM access WHERE service = \"kTCCServiceAppleEvents\""
     ],
     "infra_step": true,
-    "name": "Reset Xcode automation dialogs.Query TCC db",
+    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (4)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
-    "cmd": [
-      "ln",
-      "-s",
-      "/a/file",
-      "/a/b/c/simlink"
-    ],
-    "infra_step": true,
-    "name": "Link /a/b/c/simlink to /a/file"
-  },
-  {
-    "cmd": [
-      "killall",
-      "-9",
-      "com.apple.CoreSimulator.CoreSimulatorDevice"
-    ],
-    "infra_step": true,
-    "name": "kill dart"
-  },
-  {
+    "failure": {
+      "humanReason": "Xcode entry not found in TCC.db"
+    },
     "name": "$result"
   }
 ]
\ No newline at end of file
diff --git a/recipe_modules/os_utils/examples/full.expected/ios_debug_symbol_doctor_fails_then_succeeds.json b/recipe_modules/os_utils/examples/full.expected/ios_debug_symbol_doctor_fails_then_succeeds.json
index fe0a927..72c0fb3 100644
--- a/recipe_modules/os_utils/examples/full.expected/ios_debug_symbol_doctor_fails_then_succeeds.json
+++ b/recipe_modules/os_utils/examples/full.expected/ios_debug_symbol_doctor_fails_then_succeeds.json
@@ -555,20 +555,11 @@
       "Users/fakeuser/Library/Application Support/com.apple.TCC"
     ],
     "infra_step": true,
-    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.List TCC directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "ls",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC"
-    ],
-    "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@TCC.db@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -580,7 +571,8 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -635,7 +627,9 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (2)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@service|client|client_type|auth_value|auth_reason|auth_version|com.apple.dt.Xcode|flags|last_modified@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -659,7 +653,8 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (3)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
diff --git a/recipe_modules/os_utils/examples/full.expected/reset_dialog_xcode_automation_fails_find_db.json b/recipe_modules/os_utils/examples/full.expected/reset_dialog_xcode_automation_finds_db.json
similarity index 95%
rename from recipe_modules/os_utils/examples/full.expected/reset_dialog_xcode_automation_fails_find_db.json
rename to recipe_modules/os_utils/examples/full.expected/reset_dialog_xcode_automation_finds_db.json
index 9308ce2..6535857 100644
--- a/recipe_modules/os_utils/examples/full.expected/reset_dialog_xcode_automation_fails_find_db.json
+++ b/recipe_modules/os_utils/examples/full.expected/reset_dialog_xcode_automation_finds_db.json
@@ -521,20 +521,11 @@
       "Users/fakeuser/Library/Application Support/com.apple.TCC"
     ],
     "infra_step": true,
-    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.List TCC directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "ls",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC"
-    ],
-    "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@TCC.db@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -546,7 +537,8 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -601,7 +593,9 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (2)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@service|client|client_type|auth_value|auth_reason|auth_version|com.apple.dt.Xcode|flags|last_modified@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -625,7 +619,8 @@
     "infra_step": true,
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (3)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -682,10 +677,10 @@
       "Users/fakeuser/Library/Application Support/com.apple.TCC/TCC.db",
       "SELECT service, client, client_type, auth_value, auth_reason, indirect_object_identifier_type, indirect_object_identifier, flags, last_modified FROM access WHERE service = \"kTCCServiceAppleEvents\""
     ],
-    "infra_step": true,
     "name": "Reset Xcode automation dialogs.Query TCC db",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@1@@@"
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
diff --git a/recipe_modules/os_utils/examples/full.py b/recipe_modules/os_utils/examples/full.py
index 12d8399..29f37e5 100644
--- a/recipe_modules/os_utils/examples/full.py
+++ b/recipe_modules/os_utils/examples/full.py
@@ -45,6 +45,10 @@
       'Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory',
       stdout=api.raw_io.output_text('TCC.db'),
   )
+  xcode_dismiss_dialog_query_db_step = api.step_data(
+      'Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (2)',
+      stdout=api.raw_io.output_text('service|client|client_type|auth_value|auth_reason|auth_version|com.apple.dt.Xcode|flags|last_modified'),
+  )
   yield api.test(
       'basic',
       api.platform('win', 64),
@@ -57,6 +61,7 @@
       'ios_debug_symbol_doctor_fails_then_succeeds',
       api.step_data('ios_debug_symbol_doctor.diagnose', retcode=1),
       xcode_dismiss_dialog_find_db_step,
+      xcode_dismiss_dialog_query_db_step,
       api.platform('mac', 64),
       api.properties.environ(
           properties.EnvProperties(SWARMING_BOT_ID='flutter-devicelab-mac-1')
@@ -86,6 +91,7 @@
   yield api.test(
       'clean_derived_data', api.platform('mac', 64),
       xcode_dismiss_dialog_find_db_step,
+      xcode_dismiss_dialog_query_db_step,
       api.properties.environ(
           properties.EnvProperties(SWARMING_BOT_ID='flutter-devicelab-mac-1')
       )
@@ -103,8 +109,19 @@
       status='INFRA_FAILURE'
   )
   yield api.test(
-      'reset_dialog_xcode_automation_fails_find_db',
+      'dimiss_dialog_xcode_automation_fails_update_db',
       xcode_dismiss_dialog_find_db_step,
+      # xcode_dismiss_dialog_query_db_step,
+      api.platform('mac', 64),
+      api.properties.environ(
+          properties.EnvProperties(SWARMING_BOT_ID='flutter-devicelab-mac-1')
+      ),
+      status='INFRA_FAILURE'
+  )
+  yield api.test(
+      'reset_dialog_xcode_automation_finds_db',
+      xcode_dismiss_dialog_find_db_step,
+      xcode_dismiss_dialog_query_db_step,
       api.step_data(
           'Reset Xcode automation dialogs.Find TCC directory',
           stdout=api.raw_io.output_text('TCC.db.backup'),
diff --git a/recipe_modules/osx_sdk/api.py b/recipe_modules/osx_sdk/api.py
index 20c8658..0ce2a63 100644
--- a/recipe_modules/osx_sdk/api.py
+++ b/recipe_modules/osx_sdk/api.py
@@ -650,7 +650,8 @@
             runtime_dmg_cache_dir
         )
 
-  def _is_runtimes_unmounted(self):
+  # pylint: disable=unused-argument
+  def _is_runtimes_unmounted(self, timeout=None):
     '''Check if more than one runtime is currently mounted. If more than one
     is mounted, raise a `StepFailure`.
     '''
diff --git a/recipe_modules/repo_util/api.py b/recipe_modules/repo_util/api.py
index ca3b8bb..74f9809 100644
--- a/recipe_modules/repo_util/api.py
+++ b/recipe_modules/repo_util/api.py
@@ -128,7 +128,8 @@
         self.m.file.ensure_directory('Ensure checkout cache', checkout_path)
 
     # Inner function to execute code a second time in case of failure.
-    def _InnerCheckout():
+    # pylint: disable=unused-argument
+    def _InnerCheckout(timeout=None):
       with self.m.step.nest('Checkout source code'):
         if clobber:
           _ClobberCache()
@@ -224,7 +225,8 @@
         self.m.file.ensure_directory('Ensure checkout cache', checkout_path)
 
     # Inner function to execute code a second time in case of failure.
-    def _InnerCheckout():
+    # pylint: disable=unused-argument
+    def _InnerCheckout(timeout=None):
       with self.m.step.nest('Checkout source code'):
         if clobber:
           _ClobberCache()
diff --git a/recipe_modules/repo_util/examples/full.expected/monorepo_wrong_host.json b/recipe_modules/repo_util/examples/full.expected/monorepo_wrong_host.json
index 576304a..c76cd98 100644
--- a/recipe_modules/repo_util/examples/full.expected/monorepo_wrong_host.json
+++ b/recipe_modules/repo_util/examples/full.expected/monorepo_wrong_host.json
@@ -1144,7 +1144,7 @@
       "Traceback (most recent call last):",
       "  File \"RECIPE_REPO[flutter]/recipe_modules/repo_util/examples/full.py\", line 49, in RunSteps",
       "    api.repo_util.monorepo_checkout(checkout_path, {}, {})",
-      "  File \"RECIPE_REPO[flutter]/recipe_modules/repo_util/api.py\", line 210, in monorepo_checkout",
+      "  File \"RECIPE_REPO[flutter]/recipe_modules/repo_util/api.py\", line 211, in monorepo_checkout",
       "    raise ValueError(",
       "ValueError('Input reference is not on dart.googlesource.com/monorepo')"
     ]
diff --git a/recipe_modules/repo_util/examples/unsupported.expected/unsupported.json b/recipe_modules/repo_util/examples/unsupported.expected/unsupported.json
index 617ba98..e944c5f 100644
--- a/recipe_modules/repo_util/examples/unsupported.expected/unsupported.json
+++ b/recipe_modules/repo_util/examples/unsupported.expected/unsupported.json
@@ -9,7 +9,7 @@
       "Traceback (most recent call last):",
       "  File \"RECIPE_REPO[flutter]/recipe_modules/repo_util/examples/unsupported.py\", line 13, in RunSteps",
       "    api.repo_util.checkout('unsupported_repo', repo_dir)",
-      "  File \"RECIPE_REPO[flutter]/recipe_modules/repo_util/api.py\", line 287, in checkout",
+      "  File \"RECIPE_REPO[flutter]/recipe_modules/repo_util/api.py\", line 289, in checkout",
       "    raise ValueError('Unsupported repo: %s' % name)",
       "ValueError('Unsupported repo: unsupported_repo')"
     ]
diff --git a/recipe_modules/retry/api.py b/recipe_modules/retry/api.py
index fe6b78f..fb4c7c8 100644
--- a/recipe_modules/retry/api.py
+++ b/recipe_modules/retry/api.py
@@ -96,29 +96,33 @@
         sleep *= backoff_factor
 
   def basic_wrap(
-      self, func, max_attempts=3, sleep=5.0, backoff_factor=1.5, **kwargs
+      self, func, max_attempts=3, sleep=5.0, backoff_factor=1.5, timeout=0, **kwargs
   ):
     """Retry basic wrapped function without step support.
       Args:
           func (callable): A function that performs the action that should be
-            retried on failure. If it raises a `StepFailure`, it will be retried.
-            Any other exception will end the retry loop and bubble up.
+            retried on failure. If it raises a `StepFailure` or `InfraFailure`,
+            it will be retried. Any other exception will end the retry loop and
+            bubble up.
           max_attempts (int): How many times to try before giving up.
           sleep (int or float): The initial time to sleep between attempts.
           backoff_factor (int or float): The factor by which the sleep time
               will be multiplied after each attempt.
+          timeout (int or float): A value passed to the `func` argument. Is
+              multiplied by the `backoff_factor` after each attempt.
       Returns:
         The result of executing func.
       """
     for attempt in range(max_attempts):
       try:
-        result = func()
+        result = func(timeout=timeout)
         return result
-      except self.m.step.StepFailure:
+      except (self.m.step.StepFailure, self.m.step.InfraFailure):
         if attempt == max_attempts - 1:
           raise
         self.m.time.sleep(sleep)
         sleep *= backoff_factor
+        timeout *= backoff_factor
 
   def run_flutter_doctor(self):
     self.step(
diff --git a/recipe_modules/retry/examples/full.py b/recipe_modules/retry/examples/full.py
index 3b1929d..bd47615 100644
--- a/recipe_modules/retry/examples/full.py
+++ b/recipe_modules/retry/examples/full.py
@@ -38,7 +38,8 @@
   def func1():
     api.step('test: mytest_func', ['ls', '-a'])
 
-  def func2():
+  # pylint: disable=unused-argument
+  def func2(timeout=None):
     api.step('test: mytest_func_basic', ['ls', '-a'])
 
   api.retry.wrap(
diff --git a/recipe_modules/shard_util_v2/api.py b/recipe_modules/shard_util_v2/api.py
index 451d623..ac7c0e4 100644
--- a/recipe_modules/shard_util_v2/api.py
+++ b/recipe_modules/shard_util_v2/api.py
@@ -517,7 +517,8 @@
     cas_engine = cas_dir.join(target)
     self.m.file.copytree('Copy host_debug_unopt', build_dir, cas_engine)
 
-    def _upload():
+    # pylint: disable=unused-argument
+    def _upload(timeout=None):
       return self.m.cas_util.upload(
           cas_dir, step_name='Archive full build for %s' % target
       )
diff --git a/recipes/devicelab/devicelab_drone.expected/xcode-devicelab-timeout.json b/recipes/devicelab/devicelab_drone.expected/xcode-devicelab-timeout.json
index 990dceb..76990f2 100644
--- a/recipes/devicelab/devicelab_drone.expected/xcode-devicelab-timeout.json
+++ b/recipes/devicelab/devicelab_drone.expected/xcode-devicelab-timeout.json
@@ -1890,54 +1890,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.List TCC directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "ls",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC"
-    ],
-    "cwd": "[CLEANUP]/tmp_tmp_1/flutter sdk/dev/devicelab",
-    "env": {
-      "ARTIFACT_HUB_REPOSITORY": "artifactregistry://us-maven.pkg.dev/artifact-foundry-prod/maven-3p",
-      "DEPOT_TOOLS": "RECIPE_REPO[depot_tools]",
-      "FLUTTER_LOGS_DIR": "[CLEANUP]/flutter_logs_dir",
-      "FLUTTER_TEST_OUTPUTS_DIR": "[CLEANUP]/flutter_logs_dir",
-      "GIT_BRANCH": "master",
-      "LUCI_BRANCH": "",
-      "LUCI_CI": "True",
-      "LUCI_PR": "",
-      "OS": "darwin",
-      "PUB_CACHE": "[START_DIR]/.pub-cache",
-      "REVISION": "12345abcde12345abcde12345abcde12345abcde",
-      "SDK_CHECKOUT_PATH": "[CLEANUP]/tmp_tmp_1/flutter sdk",
-      "USE_EMULATOR": "False"
-    },
-    "env_prefixes": {
-      "PATH": [
-        "[CLEANUP]/tmp_tmp_1/flutter sdk/bin",
-        "[CLEANUP]/tmp_tmp_1/flutter sdk/bin/cache/dart-sdk/bin"
-      ]
-    },
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@TCC.db@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -1983,7 +1940,8 @@
     },
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -2174,7 +2132,9 @@
     },
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (2)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@service|client|client_type|auth_value|auth_reason|auth_version|com.apple.dt.Xcode|flags|last_modified@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -2266,7 +2226,8 @@
     },
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (3)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
diff --git a/recipes/devicelab/devicelab_drone.expected/xcode-devicelab.json b/recipes/devicelab/devicelab_drone.expected/xcode-devicelab.json
index 996c5ae..f9db4dc 100644
--- a/recipes/devicelab/devicelab_drone.expected/xcode-devicelab.json
+++ b/recipes/devicelab/devicelab_drone.expected/xcode-devicelab.json
@@ -1890,54 +1890,11 @@
         "hostname": "rdbhost"
       }
     },
-    "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.List TCC directory",
-    "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
-    ]
-  },
-  {
-    "cmd": [
-      "ls",
-      "Users/fakeuser/Library/Application Support/com.apple.TCC"
-    ],
-    "cwd": "[CLEANUP]/tmp_tmp_1/flutter sdk/dev/devicelab",
-    "env": {
-      "ARTIFACT_HUB_REPOSITORY": "artifactregistry://us-maven.pkg.dev/artifact-foundry-prod/maven-3p",
-      "DEPOT_TOOLS": "RECIPE_REPO[depot_tools]",
-      "FLUTTER_LOGS_DIR": "[CLEANUP]/flutter_logs_dir",
-      "FLUTTER_TEST_OUTPUTS_DIR": "[CLEANUP]/flutter_logs_dir",
-      "GIT_BRANCH": "master",
-      "LUCI_BRANCH": "",
-      "LUCI_CI": "True",
-      "LUCI_PR": "",
-      "OS": "darwin",
-      "PUB_CACHE": "[START_DIR]/.pub-cache",
-      "REVISION": "12345abcde12345abcde12345abcde12345abcde",
-      "SDK_CHECKOUT_PATH": "[CLEANUP]/tmp_tmp_1/flutter sdk",
-      "USE_EMULATOR": "False"
-    },
-    "env_prefixes": {
-      "PATH": [
-        "[CLEANUP]/tmp_tmp_1/flutter sdk/bin",
-        "[CLEANUP]/tmp_tmp_1/flutter sdk/bin/cache/dart-sdk/bin"
-      ]
-    },
-    "infra_step": true,
-    "luci_context": {
-      "realm": {
-        "name": "project:ci"
-      },
-      "resultdb": {
-        "current_invocation": {
-          "name": "invocations/build:8945511751514863184",
-          "update_token": "token"
-        },
-        "hostname": "rdbhost"
-      }
-    },
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@TCC.db@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -1983,7 +1940,8 @@
     },
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -2174,7 +2132,9 @@
     },
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (2)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_LINE@raw_io.output_text@service|client|client_type|auth_value|auth_reason|auth_version|com.apple.dt.Xcode|flags|last_modified@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
@@ -2266,7 +2226,8 @@
     },
     "name": "Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (3)",
     "~followup_annotations": [
-      "@@@STEP_NEST_LEVEL@2@@@"
+      "@@@STEP_NEST_LEVEL@2@@@",
+      "@@@STEP_LOG_END@raw_io.output_text@@@"
     ]
   },
   {
diff --git a/recipes/devicelab/devicelab_drone.py b/recipes/devicelab/devicelab_drone.py
index d459325..44c2dfd 100644
--- a/recipes/devicelab/devicelab_drone.py
+++ b/recipes/devicelab/devicelab_drone.py
@@ -420,6 +420,10 @@
           'Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory',
           stdout=api.raw_io.output_text('TCC.db'),
       ),
+      api.step_data(
+          'Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (2)',
+          stdout=api.raw_io.output_text('service|client|client_type|auth_value|auth_reason|auth_version|com.apple.dt.Xcode|flags|last_modified'),
+      ),
   )
   yield api.test(
       "xcode-devicelab-timeout",
@@ -447,6 +451,10 @@
           'Dismiss dialogs.Dismiss Xcode automation dialogs.Find TCC directory',
           stdout=api.raw_io.output_text('TCC.db'),
       ),
+      api.step_data(
+          'Dismiss dialogs.Dismiss Xcode automation dialogs.Query TCC db (2)',
+          stdout=api.raw_io.output_text('service|client|client_type|auth_value|auth_reason|auth_version|com.apple.dt.Xcode|flags|last_modified'),
+      ),
       api.swarming.properties(bot_id='flutter-devicelab-mac-1'),
       status='FAILURE',
   )