@@ -68,6 +68,8 @@ const APP_UPLOAD_FILE_NAME_HEADER: &str = "x-simdeck-filename";
6868const MAX_APP_UPLOAD_BYTES : usize = 1024 * 1024 * 1024 ;
6969const FOREGROUND_PROCESS_PROBE_TIMEOUT : Duration = Duration :: from_millis ( 750 ) ;
7070const ACCESSIBILITY_TREE_CACHE_TTL : Duration = Duration :: from_secs ( 5 ) ;
71+ const NATIVE_AX_SNAPSHOT_RETRY_ATTEMPTS : usize = 5 ;
72+ const NATIVE_AX_SNAPSHOT_RETRY_DELAY : Duration = Duration :: from_millis ( 100 ) ;
7173
7274static FOREGROUND_APP_CACHE : OnceLock < StdMutex < HashMap < String , CachedForegroundApp > > > =
7375 OnceLock :: new ( ) ;
@@ -3882,7 +3884,7 @@ async fn native_ax_accessibility_tree_value(
38823884 interactive_only : bool ,
38833885) -> Result < Value , AppError > {
38843886 let available_sources = available_sources_with_native_ax ( None ) ;
3885- match accessibility_snapshot_with_options ( state, udid, None , max_depth, interactive_only) . await
3887+ match accessibility_snapshot_with_retries ( state, udid, None , max_depth, interactive_only) . await
38863888 {
38873889 Ok ( native_snapshot) => {
38883890 let mut snapshot = attach_available_sources (
@@ -5090,6 +5092,12 @@ fn suppress_native_ax_translation_error(message: &str) -> Option<String> {
50905092 Some ( message. to_owned ( ) )
50915093}
50925094
5095+ fn is_transient_native_ax_snapshot_error ( message : & str ) -> bool {
5096+ message. contains ( "No application accessibility root returned for simulator" )
5097+ || message. contains ( "No translation object returned for simulator" )
5098+ || is_core_simulator_service_mismatch ( message)
5099+ }
5100+
50935101fn is_core_simulator_service_mismatch ( message : & str ) -> bool {
50945102 message. contains ( "CoreSimulator.framework was changed while the process was running" )
50955103 || message. contains ( "Service version" )
@@ -5721,7 +5729,42 @@ async fn accessibility_snapshot(
57215729 point : Option < ( f64 , f64 ) > ,
57225730 max_depth : Option < usize > ,
57235731) -> Result < Value , AppError > {
5724- accessibility_snapshot_with_options ( state, udid, point, max_depth, false ) . await
5732+ accessibility_snapshot_with_retries ( state, udid, point, max_depth, false ) . await
5733+ }
5734+
5735+ async fn accessibility_snapshot_with_retries (
5736+ state : AppState ,
5737+ udid : String ,
5738+ point : Option < ( f64 , f64 ) > ,
5739+ max_depth : Option < usize > ,
5740+ interactive_only : bool ,
5741+ ) -> Result < Value , AppError > {
5742+ let attempts = if point. is_none ( ) {
5743+ NATIVE_AX_SNAPSHOT_RETRY_ATTEMPTS
5744+ } else {
5745+ 1
5746+ } ;
5747+ for attempt in 0 ..attempts {
5748+ match accessibility_snapshot_with_options (
5749+ state. clone ( ) ,
5750+ udid. clone ( ) ,
5751+ point,
5752+ max_depth,
5753+ interactive_only,
5754+ )
5755+ . await
5756+ {
5757+ Ok ( snapshot) => return Ok ( snapshot) ,
5758+ Err ( error) => {
5759+ let message = error. to_string ( ) ;
5760+ if attempt + 1 >= attempts || !is_transient_native_ax_snapshot_error ( & message) {
5761+ return Err ( error) ;
5762+ }
5763+ tokio:: time:: sleep ( NATIVE_AX_SNAPSHOT_RETRY_DELAY ) . await ;
5764+ }
5765+ }
5766+ }
5767+ unreachable ! ( "native AX snapshot retry loop always returns" )
57255768}
57265769
57275770async fn accessibility_snapshot_with_options (
@@ -5745,9 +5788,9 @@ mod tests {
57455788 compact_accessibility_snapshot, element_matches_selector, first_matching_element,
57465789 inspector_available_sources, inspector_metadata, inspector_session_from_published,
57475790 inspector_session_score, is_inspector_agent_transport_path,
5748- logical_screen_size_from_display_pixels , normalize_inspector_node ,
5749- normalize_screen_point_from_snapshot , normalized_gesture_coordinates ,
5750- parse_lsof_tcp_listener, parse_ui_application_service_line,
5791+ is_transient_native_ax_snapshot_error , logical_screen_size_from_display_pixels ,
5792+ normalize_inspector_node , normalize_screen_point_from_snapshot ,
5793+ normalized_gesture_coordinates , parse_lsof_tcp_listener, parse_ui_application_service_line,
57515794 process_identifier_from_accessibility_snapshot, resolved_stream_quality_limits,
57525795 scroll_input_plan_for_udid, split_filter_values, stream_quality_profile,
57535796 suppress_native_ax_translation_error, tap_point_from_snapshot, trim_tree_depth,
@@ -6246,6 +6289,17 @@ mod tests {
62466289 assert ! ( suppress_native_ax_translation_error( "Bridge failed" ) . is_some( ) ) ;
62476290 }
62486291
6292+ #[ test]
6293+ fn transient_native_ax_snapshot_errors_are_retryable ( ) {
6294+ assert ! ( is_transient_native_ax_snapshot_error(
6295+ "No application accessibility root returned for simulator. The simulator may be between lifecycle states."
6296+ ) ) ;
6297+ assert ! ( is_transient_native_ax_snapshot_error(
6298+ "No translation object returned for simulator SIM"
6299+ ) ) ;
6300+ assert ! ( !is_transient_native_ax_snapshot_error( "Bridge failed" ) ) ;
6301+ }
6302+
62496303 #[ test]
62506304 fn parse_lsof_tcp_listener_extracts_pid_and_port ( ) {
62516305 assert_eq ! (
0 commit comments