Skip to content

Commit e070c11

Browse files
committed
fix: retry transient native ax empty roots
1 parent e5d495a commit e070c11

1 file changed

Lines changed: 59 additions & 5 deletions

File tree

server/src/api/routes.rs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ const APP_UPLOAD_FILE_NAME_HEADER: &str = "x-simdeck-filename";
6868
const MAX_APP_UPLOAD_BYTES: usize = 1024 * 1024 * 1024;
6969
const FOREGROUND_PROCESS_PROBE_TIMEOUT: Duration = Duration::from_millis(750);
7070
const 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

7274
static 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+
50935101
fn 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

57275770
async 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

Comments
 (0)