@smbcloud / cli / commits / 074ea15

Wait for AndroidKit readiness after startup

Co-Authored-By: siGit Code <noreply@sigit.si>

Seto Elkahfi committed Sep 10, 2026 at 15:47 UTC 074ea15f181432413d8c00b66526e886ded80db4
2 files changed +59 -6
crates/xcrs/src/main.rs
+1 -1
@@ -94,7 +94,7 @@ async fn main() -> Result<()> {
94 println!("Installed XCRS AndroidKit on {serial}");
95 }
96 Commands::AndroidKit(AndroidKitCommand::Start { serial }) => {
97 - AndroidDebugBridge::new().start_androidkit(&serial)?;
97 + AndroidDebugBridge::new().start_androidkit(&serial).await?;
98 println!("Started XCRS AndroidKit on {serial}");
99 }
100 Commands::AndroidKit(AndroidKitCommand::Status { serial, json }) => {
crates/xcrs/src/xcrs.rs
+58 -5
@@ -12,6 +12,10 @@ pub mod mcp;
12 const IOS_SIMULATOR_DESTINATION_PREFIX: &str = "platform=iOS Simulator,id=";
13 const CONTROLKIT_METHOD_NOT_FOUND: i64 = -32601;
14 const CONTROLKIT_RUNNER_INFO_TIMEOUT: Duration = Duration::from_secs(2);
15 +const ANDROIDKIT_CALL_TIMEOUT: Duration = Duration::from_secs(10);
16 +const ANDROIDKIT_START_TIMEOUT: Duration = Duration::from_secs(10);
17 +const ANDROIDKIT_START_PROBE_TIMEOUT: Duration = Duration::from_millis(500);
18 +const ANDROIDKIT_START_RETRY_DELAY: Duration = Duration::from_millis(100);
19
20 pub fn encode_base64(data: impl AsRef<[u8]>) -> String {
21 use base64::{engine::general_purpose::STANDARD, Engine};
@@ -689,12 +693,14 @@ pub struct AndroidDebugBridge {
693 #[derive(Debug, Clone)]
694 pub struct AndroidKit {
695 client: reqwest::Client,
696 + adb_path: PathBuf,
697 }
698
699 impl Default for AndroidKit {
700 fn default() -> Self {
701 Self {
702 client: reqwest::Client::new(),
703 + adb_path: discover_adb_path(),
704 }
705 }
706 }
@@ -712,7 +718,25 @@ impl AndroidKit {
718 method: &str,
719 params: serde_json::Value,
720 ) -> Result<serde_json::Value> {
715 - let bridge = AndroidDebugBridge::new();
721 + self.call_with_timeout(serial, method, params, ANDROIDKIT_CALL_TIMEOUT)
722 + .await
723 + }
724 +
725 + fn with_adb_path(adb_path: impl Into<PathBuf>) -> Self {
726 + Self {
727 + client: reqwest::Client::new(),
728 + adb_path: adb_path.into(),
729 + }
730 + }
731 +
732 + async fn call_with_timeout(
733 + &self,
734 + serial: String,
735 + method: &str,
736 + params: serde_json::Value,
737 + timeout: Duration,
738 + ) -> Result<serde_json::Value> {
739 + let bridge = AndroidDebugBridge::with_path(self.adb_path.clone());
740 let local_port = tokio::task::spawn_blocking({
741 let serial = serial.clone();
742 move || bridge.forward_androidkit(&serial)
@@ -723,7 +747,7 @@ impl AndroidKit {
747 let response = self
748 .client
749 .post(format!("http://127.0.0.1:{local_port}/rpc"))
726 - .timeout(Duration::from_secs(10))
750 + .timeout(timeout)
751 .json(&serde_json::json!({
752 "jsonrpc": "2.0",
753 "id": 1,
@@ -734,7 +758,7 @@ impl AndroidKit {
758 .await
759 .with_context(|| format!("failed to connect to XCRS AndroidKit for {serial}"));
760
737 - let cleanup_bridge = AndroidDebugBridge::new();
761 + let cleanup_bridge = AndroidDebugBridge::with_path(self.adb_path.clone());
762 let cleanup_serial = serial.clone();
763 let cleanup = tokio::task::spawn_blocking(move || {
764 cleanup_bridge.remove_forward(&cleanup_serial, local_port)
@@ -769,6 +793,32 @@ impl AndroidKit {
793 .cloned()
794 .ok_or_else(|| anyhow!("AndroidKit response for '{method}' did not contain a result"))
795 }
796 +
797 + async fn wait_until_ready(&self, serial: &str) -> Result<()> {
798 + let deadline = tokio::time::Instant::now() + ANDROIDKIT_START_TIMEOUT;
799 + loop {
800 + match self
801 + .call_with_timeout(
802 + serial.to_string(),
803 + "device.info",
804 + serde_json::json!({}),
805 + ANDROIDKIT_START_PROBE_TIMEOUT,
806 + )
807 + .await
808 + {
809 + Ok(_) => return Ok(()),
810 + Err(error) if tokio::time::Instant::now() >= deadline => {
811 + return Err(error).with_context(|| {
812 + format!(
813 + "AndroidKit did not become ready on {serial} within {} seconds",
814 + ANDROIDKIT_START_TIMEOUT.as_secs()
815 + )
816 + });
817 + }
818 + Err(_) => tokio::time::sleep(ANDROIDKIT_START_RETRY_DELAY).await,
819 + }
820 + }
821 + }
822 }
823
824 impl Default for AndroidDebugBridge {
@@ -956,13 +1006,16 @@ impl AndroidDebugBridge {
1006 Ok(())
1007 }
1008
959 - pub fn start_androidkit(&self, serial: &str) -> Result<()> {
1009 + pub async fn start_androidkit(&self, serial: &str) -> Result<()> {
1010 self.run_shell(
1011 serial,
1012 "nohup am instrument -w -r xyz.smbcloud.xcrs.androidkit/.AndroidKitInstrumentation >/dev/null 2>&1 &",
1013 )
1014 .with_context(|| format!("failed to start AndroidKit on {serial}"))?;
965 - Ok(())
1015 + AndroidKit::with_adb_path(self.adb_path.clone())
1016 + .wait_until_ready(serial)
1017 + .await
1018 + .with_context(|| format!("failed to start AndroidKit on {serial}"))
1019 }
1020
1021 pub fn stop_androidkit(&self, serial: &str) -> Result<()> {