1use std::io::{self, BufRead};
7use std::net::{Ipv4Addr, SocketAddrV4};
8use std::path::{Path, PathBuf};
9use std::process::{Child, Command, Stdio};
10use std::sync::Arc;
11use std::thread::{self, JoinHandle};
12use std::time::Duration;
13use thiserror::Error;
14
15pub mod auth;
16pub mod proto;
17
18pub use proto::emulator_controller_client::EmulatorControllerClient;
19use tonic::transport::Channel;
20
21use crate::auth::AuthProvider;
22
23#[doc = include_str!("../README.md")]
24#[cfg(doctest)]
25pub struct ReadmeDoctests;
26
27#[derive(Error, Debug)]
28pub enum EmulatorError {
29 #[error(
30 "Android SDK not found. Checked:\n - ANDROID_HOME environment variable\n - ANDROID_SDK_ROOT environment variable\n - Platform default locations (e.g., ~/Android/sdk)\nPlease install the Android SDK or set ANDROID_HOME"
31 )]
32 AndroidHomeNotFound,
33
34 #[error("No emulator AVDs found")]
35 NoAvdsFound,
36
37 #[error("Failed to spawn or connect to ADB server: {0}")]
38 AdbError(String),
39
40 #[error("Android SDK emulator tool not found at path: {0}")]
41 EmulatorToolNotFound(String),
42
43 #[error("Invalid gRPC endpoint URI: {0}")]
44 InvalidUri(String),
45
46 #[error("Failed to enumerate running emulators: {0}")]
47 EnumerationFailed(String),
48
49 #[error("Failed to start emulator: {0}")]
50 EmulatorStartFailed(String),
51
52 #[error("Emulator connection timed out")]
53 ConnectionTimeout,
54
55 #[error("Authentication error: {0}")]
56 AuthError(#[from] crate::auth::AuthError),
57
58 #[error("gRPC connection error: {0}")]
59 GrpcError(#[from] tonic::transport::Error),
60
61 #[error("gRPC status error: {0}")]
62 GrpcStatus(#[from] tonic::Status),
63
64 #[error("IO error: {0}")]
65 IoError(#[from] std::io::Error),
66}
67
68pub type Result<T> = std::result::Result<T, EmulatorError>;
69
70async fn find_free_grpc_port() -> Option<u16> {
74 use adb_client::emulator::ADBEmulatorDevice;
75 use std::collections::HashSet;
76
77 let mut server = adb_server().await.ok()?;
78
79 tokio::task::spawn_blocking(move || {
80 let devices = match server.devices() {
81 Ok(d) => d,
82 Err(_) => return None,
83 };
84
85 let mut used_ports = HashSet::new();
87 for device in devices {
88 if device.identifier.starts_with("emulator-")
89 && let Ok(mut emulator_device) = ADBEmulatorDevice::new(device.identifier, None)
90 && let Ok(discovery_path) = emulator_device.avd_discovery_path()
91 && let Ok(ini_content) = std::fs::read_to_string(&discovery_path)
92 {
93 let metadata = parse_ini(&ini_content);
94 if let Some(port_str) = metadata.get("grpc.port")
95 && let Ok(port) = port_str.parse::<u16>()
96 {
97 used_ports.insert(port);
98 }
99 }
100 }
101
102 (8554..8600).find(|&port| !used_ports.contains(&port))
104 })
105 .await
106 .ok()?
107}
108
109fn log_emulator_line(line: &str) {
111 let trimmed = line.trim_start();
112
113 if let Some(rest) = trimmed.strip_prefix("ERROR ") {
114 tracing::error!("{}", rest.trim_start());
115 } else if let Some(rest) = trimmed.strip_prefix("WARNING ") {
116 tracing::warn!("{}", rest.trim_start());
117 } else if let Some(rest) = trimmed.strip_prefix("WARN ") {
118 tracing::warn!("{}", rest.trim_start());
119 } else if let Some(rest) = trimmed.strip_prefix("INFO ") {
120 tracing::info!("{}", rest.trim_start());
121 } else if let Some(rest) = trimmed.strip_prefix("DEBUG ") {
122 tracing::debug!("{}", rest.trim_start());
123 } else if let Some(rest) = trimmed.strip_prefix("TRACE ") {
124 tracing::trace!("{}", rest.trim_start());
125 } else {
126 tracing::debug!("{}", line);
128 }
129}
130
131#[derive(Debug, Clone)]
133pub enum GrpcAuthConfig {
134 None,
136 Basic,
138 Jwt {
140 issuer: Option<String>,
142 },
143}
144
145impl Default for GrpcAuthConfig {
146 fn default() -> Self {
147 GrpcAuthConfig::Jwt { issuer: None }
148 }
149}
150
151#[derive(Debug)]
153pub struct EmulatorConfig {
154 avd_name: String,
156 grpc_port: Option<u16>,
158 grpc_auth: GrpcAuthConfig,
160 no_window: bool,
162 no_snapshot_load: bool,
164 no_snapshot_save: bool,
166 no_boot_anim: bool,
168 no_acceleration: bool,
170 dalvik_vm_check_jni: bool,
172 read_only: bool,
174 quit_after_boot: Option<Duration>,
176 extra_args: Vec<String>,
178 grpc_allowlist: Option<auth::GrpcAllowlist>,
181 stdout: Option<Stdio>,
183 stderr: Option<Stdio>,
185}
186
187impl EmulatorConfig {
188 pub fn new(avd_name: impl Into<String>) -> Self {
190 Self {
191 avd_name: avd_name.into(),
192 grpc_port: None,
193 grpc_auth: GrpcAuthConfig::default(),
194 no_window: true,
195 no_snapshot_load: false,
196 no_snapshot_save: false,
197 no_boot_anim: false,
198 no_acceleration: false,
199 dalvik_vm_check_jni: false,
200 read_only: false,
201 quit_after_boot: None,
202 extra_args: Vec::new(),
203 grpc_allowlist: None,
204 stdout: None,
205 stderr: None,
206 }
207 }
208
209 pub fn avd_id(&self) -> &str {
211 &self.avd_name
212 }
213
214 async fn poll_for_emulator(
216 grpc_port: u16,
217 ) -> Result<(String, std::collections::HashMap<String, String>, PathBuf)> {
218 use adb_client::emulator::ADBEmulatorDevice;
219
220 let mut server = adb_server().await?;
221 tokio::task::spawn_blocking(move || {
222 loop {
223 std::thread::sleep(Duration::from_millis(500));
224
225 let devices = match server.devices() {
226 Ok(d) => d,
227 Err(_) => continue,
228 };
229
230 for device in devices {
231 if !device.identifier.starts_with("emulator-") {
232 continue;
233 }
234
235 let mut emulator_device =
236 match ADBEmulatorDevice::new(device.identifier.clone(), None) {
237 Ok(d) => d,
238 Err(_) => continue,
239 };
240
241 let discovery_path = match emulator_device.avd_discovery_path() {
242 Ok(p) => p,
243 Err(_) => continue,
244 };
245
246 let ini_content = match std::fs::read_to_string(&discovery_path) {
247 Ok(c) => c,
248 Err(_) => continue,
249 };
250
251 let metadata = parse_ini(&ini_content);
252
253 if let Some(port_str) = metadata.get("grpc.port")
255 && let Ok(found_port) = port_str.parse::<u16>()
256 && found_port == grpc_port
257 {
258 return Ok((device.identifier, metadata, discovery_path));
259 }
260 }
261 }
262 })
263 .await
264 .map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?
265 }
266
267 pub fn with_grpc_auth(mut self, auth: GrpcAuthConfig) -> Self {
309 self.grpc_auth = auth;
310 self
311 }
312
313 pub fn with_grpc_port(mut self, port: u16) -> Self {
326 self.grpc_port = Some(port);
327 self
328 }
329
330 pub fn with_window(mut self, show: bool) -> Self {
334 self.no_window = !show;
335 self
336 }
337
338 pub fn with_snapshot_load(mut self, load: bool) -> Self {
343 self.no_snapshot_load = !load;
344 self
345 }
346
347 pub fn with_snapshot_save(mut self, save: bool) -> Self {
352 self.no_snapshot_save = !save;
353 self
354 }
355
356 pub fn with_boot_animation(mut self, show: bool) -> Self {
361 self.no_boot_anim = !show;
362 self
363 }
364
365 pub fn with_acceleration(mut self, enable: bool) -> Self {
371 self.no_acceleration = !enable;
372 self
373 }
374
375 pub fn with_dalvik_vm_check_jni(mut self, enable: bool) -> Self {
381 self.dalvik_vm_check_jni = enable;
382 self
383 }
384
385 pub fn with_read_only(mut self, read_only: bool) -> Self {
392 self.read_only = read_only;
393 self
394 }
395
396 pub fn with_quit_after_boot(mut self, duration: Option<Duration>) -> Self {
405 self.quit_after_boot = duration;
406 self
407 }
408
409 pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
410 self.extra_args = args;
411 self
412 }
413
414 pub fn with_grpc_allowlist(mut self, allowlist: auth::GrpcAllowlist) -> Self {
469 self.grpc_allowlist = Some(allowlist);
470 self
471 }
472
473 pub fn stdout<T: Into<Stdio>>(mut self, cfg: T) -> Self {
475 self.stdout = Some(cfg.into());
476 self
477 }
478
479 pub fn stderr<T: Into<Stdio>>(mut self, cfg: T) -> Self {
481 self.stderr = Some(cfg.into());
482 self
483 }
484
485 pub async fn spawn(self) -> Result<Emulator> {
487 let android_home = get_android_home().await?;
488 let emulator_path = android_home.join("emulator").join("emulator");
489
490 if !tokio::fs::try_exists(&emulator_path).await.unwrap_or(false) {
491 return Err(EmulatorError::EmulatorToolNotFound(
492 emulator_path.display().to_string(),
493 ));
494 }
495
496 let mut cmd = Command::new(&emulator_path);
497 cmd.arg("-avd").arg(&self.avd_name);
498
499 if self.no_window {
500 cmd.arg("-no-window");
501 }
502
503 if self.no_snapshot_load {
504 cmd.arg("-no-snapshot-load");
505 }
506
507 if self.no_acceleration {
508 cmd.arg("-accel").arg("off");
509 }
510
511 if self.no_boot_anim {
512 cmd.arg("-no-boot-anim");
513 }
514
515 if self.dalvik_vm_check_jni {
516 cmd.arg("-dalvik-vm-checkjni");
517 }
518
519 if self.read_only {
520 cmd.arg("-read-only");
521 }
522
523 if let Some(quit_after) = self.quit_after_boot {
524 cmd.arg("-quit-after-boot")
525 .arg(quit_after.as_secs().to_string());
526 }
527
528 let use_default_stdout = self.stdout.is_none();
530 let use_default_stderr = self.stderr.is_none();
531
532 if let Some(stdout) = self.stdout {
534 cmd.stdout(stdout);
535 } else {
536 cmd.stdout(std::process::Stdio::piped());
537 }
538
539 if let Some(stderr) = self.stderr {
541 cmd.stderr(stderr);
542 } else {
543 cmd.stderr(std::process::Stdio::piped());
544 }
545
546 let grpc_port = match self.grpc_port {
549 Some(port) => port,
550 None => find_free_grpc_port().await.unwrap_or(8554),
551 };
552 cmd.arg("-grpc").arg(grpc_port.to_string());
553
554 let issuer = match self.grpc_auth {
555 GrpcAuthConfig::None => {
556 None
558 }
559 GrpcAuthConfig::Basic => {
560 cmd.arg("-grpc-use-token");
562 None
563 }
564 GrpcAuthConfig::Jwt { issuer } => {
565 let issuer = issuer.unwrap_or_else(|| format!("emulator-{}", grpc_port));
567
568 let allowlist = self
570 .grpc_allowlist
571 .unwrap_or_else(|| auth::GrpcAllowlist::default_for_issuer(&issuer));
572
573 let allowlist_json = serde_json::to_string_pretty(&allowlist).map_err(|e| {
575 EmulatorError::EmulatorStartFailed(format!(
576 "Failed to serialize allowlist: {}",
577 e
578 ))
579 })?;
580
581 let temp_dir = std::env::temp_dir();
582 let allowlist_path =
583 temp_dir.join(format!("emulator-allowlist-{}.json", std::process::id()));
584 tokio::fs::write(&allowlist_path, allowlist_json).await?;
585
586 cmd.arg("-grpc-allowlist").arg(&allowlist_path);
587
588 cmd.arg("-grpc-use-jwt");
590
591 Some(issuer)
592 }
593 };
594
595 for arg in &self.extra_args {
596 cmd.arg(arg);
597 }
598
599 let mut process = cmd
600 .spawn()
601 .map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
602
603 let stdout_thread = if use_default_stdout {
605 let child_out = process.stdout.take().expect("stdout should be piped");
606
607 Some(thread::spawn(move || -> io::Result<()> {
608 let _span = tracing::info_span!("emulator").entered();
609 let reader = io::BufReader::new(child_out);
610 for line in reader.lines() {
611 let line = line?;
612 log_emulator_line(&line);
613 }
614 Ok(())
615 }))
616 } else {
617 None
618 };
619
620 let stderr_thread = if use_default_stderr {
622 let child_err = process.stderr.take().expect("stderr should be piped");
623
624 Some(thread::spawn(move || -> io::Result<()> {
625 let _span = tracing::info_span!("emulator").entered();
626 let reader = io::BufReader::new(child_err);
627 for line in reader.lines() {
628 let line = line?;
629 log_emulator_line(&line);
630 }
631 Ok(())
632 }))
633 } else {
634 None
635 };
636
637 let (serial, metadata, discovery_path) = Self::poll_for_emulator(grpc_port).await?;
639
640 Ok(Emulator {
641 is_owned: true,
642 process: tokio::sync::Mutex::new(Some(process)),
643 grpc_port,
644 serial,
645 metadata,
646 discovery_path,
647 issuer,
648 stdout_thread: tokio::sync::Mutex::new(stdout_thread),
649 stderr_thread: tokio::sync::Mutex::new(stderr_thread),
650 })
651 }
652}
653
654pub struct EmulatorClient {
659 provider: auth::AuthProvider,
660 interceptor: EmulatorControllerClient<
661 tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
662 >,
663 endpoint: String,
664}
665
666impl EmulatorClient {
667 pub async fn connect_avd(avd: &str) -> Result<Self> {
669 let emulators = list_emulators().await?;
670 let matching = emulators
671 .into_iter()
672 .find(|e| e.avd_id().map(|id| id == avd).unwrap_or(false));
673 if let Some(emulator) = matching {
674 emulator.connect(Some(Duration::from_secs(30)), true).await
675 } else {
676 Err(EmulatorError::EmulatorStartFailed(
677 "No running emulator found".to_string(),
678 ))
679 }
680 }
681
682 pub async fn connect(endpoint: impl Into<String>) -> Result<Self> {
684 let endpoint = endpoint.into();
685 let channel = Channel::from_shared(endpoint.clone())
686 .map_err(|e| EmulatorError::InvalidUri(e.to_string()))?
687 .connect()
688 .await?;
689
690 let provider = std::sync::Arc::new(auth::NoOpTokenProvider);
691 let provider = auth::AuthProvider::new_with_token_provider(provider);
692
693 Ok(Self {
694 interceptor: EmulatorControllerClient::with_interceptor(channel, provider.clone()),
695 provider,
696 endpoint,
697 })
698 }
699
700 pub async fn connect_with_auth(
702 endpoint: impl Into<String>,
703 provider: auth::AuthProvider,
704 ) -> Result<Self> {
705 let endpoint = endpoint.into();
706 let channel = Channel::from_shared(endpoint.clone())
707 .map_err(|e| EmulatorError::InvalidUri(e.to_string()))?
708 .connect()
709 .await?;
710
711 Ok(Self {
712 interceptor: EmulatorControllerClient::with_interceptor(channel, provider.clone()),
713 provider,
714 endpoint,
715 })
716 }
717
718 pub fn auth_scheme(&self) -> &auth::AuthScheme {
720 self.provider.auth_scheme()
721 }
722
723 pub fn export_token(&self, auds: &[&str], ttl: Duration) -> Result<auth::BearerToken> {
735 let token = self.provider.export_token(auds, ttl)?;
736 Ok(token)
737 }
738
739 pub fn endpoint(&self) -> &str {
741 &self.endpoint
742 }
743
744 pub fn protocol_mut(
746 &mut self,
747 ) -> &mut EmulatorControllerClient<
748 tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
749 > {
750 &mut self.interceptor
751 }
752
753 pub fn protocol(
755 &self,
756 ) -> &EmulatorControllerClient<
757 tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
758 > {
759 &self.interceptor
760 }
761
762 pub async fn wait_until_booted(
798 &mut self,
799 timeout: Duration,
800 poll_interval: Option<Duration>,
801 ) -> Result<Duration> {
802 let poll_interval = poll_interval.unwrap_or(Duration::from_secs(2));
803 let start = std::time::Instant::now();
804 let mut attempt = 0;
805
806 loop {
807 attempt += 1;
808 let status = self.protocol_mut().get_status(()).await?.into_inner();
809
810 if status.booted {
811 let elapsed = start.elapsed();
812 tracing::info!(
813 "Emulator fully booted after {:.1} seconds ({} attempts)",
814 elapsed.as_secs_f64(),
815 attempt
816 );
817 return Ok(elapsed);
818 }
819
820 tracing::debug!(
821 "Boot status: {} (attempt {}, elapsed: {:.1}s)",
822 status.booted,
823 attempt,
824 start.elapsed().as_secs_f64()
825 );
826
827 if start.elapsed() >= timeout {
829 return Err(EmulatorError::ConnectionTimeout);
830 }
831
832 let remaining = timeout.saturating_sub(start.elapsed());
834 let sleep_duration = poll_interval.min(remaining);
835
836 if sleep_duration.is_zero() {
837 return Err(EmulatorError::ConnectionTimeout);
838 }
839
840 tokio::time::sleep(sleep_duration).await;
841 }
842 }
843}
844
845#[derive(Debug)]
850pub struct Emulator {
851 is_owned: bool,
852 process: tokio::sync::Mutex<Option<Child>>,
853 serial: String,
854 grpc_port: u16,
855 discovery_path: PathBuf,
857 metadata: std::collections::HashMap<String, String>,
859 issuer: Option<String>,
861 stdout_thread: tokio::sync::Mutex<Option<JoinHandle<io::Result<()>>>>,
863 stderr_thread: tokio::sync::Mutex<Option<JoinHandle<io::Result<()>>>>,
865}
866
867impl Emulator {
868 pub fn serial(&self) -> &str {
870 &self.serial
871 }
872
873 pub fn is_owned(&self) -> bool {
875 self.is_owned
876 }
877
878 pub fn discovery_path(&self) -> &Path {
879 self.discovery_path.as_path()
880 }
881
882 pub fn metadata(&self) -> &std::collections::HashMap<String, String> {
884 &self.metadata
885 }
886
887 pub fn get_metadata(&self, key: &str) -> Option<&str> {
889 self.metadata.get(key).map(|s| s.as_str())
890 }
891
892 pub fn requires_jwt_auth(&self) -> bool {
894 self.get_metadata("grpc.jwk_active").is_some()
895 }
896
897 pub fn avd_name(&self) -> Option<&str> {
899 self.get_metadata("avd.name")
900 }
901
902 pub fn avd_id(&self) -> Option<&str> {
904 self.get_metadata("avd.id")
905 }
906
907 pub fn avd_dir(&self) -> Option<&str> {
909 self.get_metadata("avd.dir")
910 }
911
912 pub fn emulator_version(&self) -> Option<&str> {
914 self.get_metadata("emulator.version")
915 }
916
917 pub fn emulator_build(&self) -> Option<&str> {
919 self.get_metadata("emulator.build")
920 }
921
922 pub fn port_serial(&self) -> Option<u16> {
924 self.get_metadata("port.serial")?.parse().ok()
925 }
926
927 pub fn port_adb(&self) -> Option<u16> {
929 self.get_metadata("port.adb")?.parse().ok()
930 }
931
932 pub fn cmdline(&self) -> Option<&str> {
934 self.get_metadata("cmdline")
935 }
936
937 pub fn grpc_endpoint(&self) -> String {
939 format!("http://localhost:{}", self.grpc_port)
940 }
941
942 pub fn grpc_port(&self) -> u16 {
944 self.grpc_port
945 }
946
947 pub async fn connect(
962 &self,
963 timeout: Option<Duration>,
964 allow_basic_auth: bool,
965 ) -> Result<EmulatorClient> {
966 let basic_auth_token = self.get_metadata("grpc.token");
968
969 if self.requires_jwt_auth() {
970 tracing::info!(
971 "Emulator requires JWT authentication, setting up ES256 token provider..."
972 );
973
974 match self.connect_with_jwt_auth(timeout).await {
976 Ok(client) => {
977 tracing::info!("Connected to emulator with JWT authentication.");
978 return Ok(client);
979 }
980 Err(err) => {
981 tracing::error!("Failed to connect with JWT authentication: {}", err);
982 if basic_auth_token.is_some() && allow_basic_auth {
983 tracing::warn!("Falling back to basic authentication...");
984 } else {
985 return Err(err);
986 }
987 }
988 }
989 } else {
990 tracing::info!("Emulator does not require JWT authentication.");
991 }
992
993 if allow_basic_auth && let Some(token) = basic_auth_token {
996 tracing::info!("Emulator accepts basic auth, setting up BasicAuthTokenProvider...");
997 return self.connect_with_basic_auth(token, timeout).await;
998 }
999
1000 self.connect_with_noop_auth(timeout).await
1002 }
1003
1004 async fn connect_with_noop_auth(&self, timeout: Option<Duration>) -> Result<EmulatorClient> {
1006 let start = std::time::Instant::now();
1007
1008 let provider = Arc::new(auth::NoOpTokenProvider);
1009 let provider = AuthProvider::new_with_token_provider(provider);
1010 loop {
1011 match EmulatorClient::connect_with_auth(self.grpc_endpoint(), provider.clone()).await {
1012 Ok(mut client) => {
1013 if client.protocol_mut().get_status(()).await.is_ok() {
1015 return Ok(client);
1016 }
1017 }
1018 Err(err) => {
1019 tracing::error!("No-auth connection attempt failed: {}", err);
1020 }
1021 }
1022
1023 if let Some(timeout_duration) = timeout
1025 && start.elapsed() > timeout_duration
1026 {
1027 return Err(EmulatorError::ConnectionTimeout);
1028 }
1029
1030 tokio::time::sleep(Duration::from_secs(1)).await;
1031 }
1032 }
1033
1034 async fn connect_with_basic_auth(
1036 &self,
1037 token: &str,
1038 timeout: Option<Duration>,
1039 ) -> Result<EmulatorClient> {
1040 let start = std::time::Instant::now();
1041
1042 let provider = Arc::new(auth::BearerTokenProvider::new(token.to_string()));
1043 let provider = AuthProvider::new_with_token_provider(provider);
1044
1045 loop {
1046 match EmulatorClient::connect_with_auth(self.grpc_endpoint(), provider.clone()).await {
1047 Ok(mut client) => {
1048 if client.protocol_mut().get_status(()).await.is_ok() {
1050 return Ok(client);
1051 }
1052 }
1053 Err(err) => {
1054 tracing::error!("Basic auth connection attempt failed: {}", err);
1055 }
1056 }
1057
1058 if let Some(timeout_duration) = timeout
1060 && start.elapsed() > timeout_duration
1061 {
1062 return Err(EmulatorError::ConnectionTimeout);
1063 }
1064
1065 tokio::time::sleep(Duration::from_secs(1)).await;
1066 }
1067 }
1068
1069 async fn connect_with_jwt_auth(&self, timeout: Option<Duration>) -> Result<EmulatorClient> {
1074 let jwks_path = self.get_metadata("grpc.jwks").ok_or_else(|| {
1076 EmulatorError::EmulatorStartFailed(
1077 "Emulator requires JWT auth but grpc.jwks path not found in metadata".to_string(),
1078 )
1079 })?;
1080
1081 let jwks_dir = PathBuf::from(jwks_path);
1082
1083 let issuer = self.issuer.as_deref().unwrap_or("android-studio");
1084
1085 let provider = auth::JwtTokenProvider::new_and_register(&jwks_dir, issuer)?;
1087
1088 provider.wait_for_activation(&jwks_dir, Duration::from_secs(10))?;
1090
1091 let provider = AuthProvider::new_with_token_provider(provider);
1092
1093 let start = std::time::Instant::now();
1094
1095 loop {
1096 tracing::info!("Attempting JWT connection...");
1097 match EmulatorClient::connect_with_auth(self.grpc_endpoint(), provider.clone()).await {
1098 Ok(mut client) => {
1099 tracing::info!("JWT authentication successful.");
1100 match client.protocol_mut().get_status(()).await {
1102 Ok(_) => {
1103 tracing::info!(
1104 "Successfully connected to emulator with JWT authentication."
1105 );
1106 return Ok(client);
1107 }
1108 Err(err) => {
1109 tracing::error!(
1110 "Failed to get status with JWT authentication: {}",
1111 err
1112 );
1113 }
1114 }
1115 }
1116 Err(err) => {
1117 tracing::error!("JWT connection attempt failed: {}", err);
1118 }
1119 }
1120
1121 if let Some(timeout_duration) = timeout
1123 && start.elapsed() > timeout_duration
1124 {
1125 return Err(EmulatorError::ConnectionTimeout);
1126 }
1127 tracing::info!("Sleeping before retrying JWT connection...");
1128 tokio::time::sleep(Duration::from_secs(1)).await;
1129 }
1130 }
1131
1132 pub async fn terminate(&self) -> Result<()> {
1142 let mut lock = self.process.lock().await;
1143 if let Some(mut process) = lock.take() {
1144 return tokio::task::spawn_blocking(move || {
1145 process.kill()?;
1147
1148 let _ = process.wait()?;
1150
1151 Ok(())
1152 })
1153 .await
1154 .map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?;
1155 }
1156
1157 Err(EmulatorError::EmulatorStartFailed(
1159 "Cannot terminate emulator: process not owned by this instance".to_string(),
1160 ))
1161 }
1162}
1163
1164impl Drop for Emulator {
1165 fn drop(&mut self) {
1166 if let Some(ref mut process) = *self.process.get_mut() {
1169 let _ = process.kill();
1170 }
1171
1172 if let Some(out_thread) = self.stdout_thread.get_mut().take() {
1174 let _ = out_thread.join();
1175 }
1176 if let Some(err_thread) = self.stderr_thread.get_mut().take() {
1177 let _ = err_thread.join();
1178 }
1179 }
1180}
1181
1182pub async fn get_android_home() -> Result<PathBuf> {
1192 if let Ok(path) = std::env::var("ANDROID_HOME") {
1194 return Ok(PathBuf::from(path));
1195 }
1196
1197 if let Ok(path) = std::env::var("ANDROID_SDK_ROOT") {
1198 return Ok(PathBuf::from(path));
1199 }
1200
1201 #[cfg(target_os = "linux")]
1203 {
1204 if let Some(home) = dirs::home_dir() {
1205 let sdk_path = home.join("Android").join("sdk");
1207 if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1208 return Ok(sdk_path);
1209 }
1210
1211 let sdk_path = home.join("Android").join("Sdk");
1213 if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1214 return Ok(sdk_path);
1215 }
1216 }
1217 }
1218
1219 #[cfg(target_os = "macos")]
1220 {
1221 if let Some(home) = dirs::home_dir() {
1222 let sdk_path = home.join("Library").join("Android").join("sdk");
1223 if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1224 return Ok(sdk_path);
1225 }
1226 }
1227 }
1228
1229 #[cfg(target_os = "windows")]
1230 {
1231 if let Some(local_data) = dirs::data_local_dir() {
1232 let sdk_path = local_data.join("Android").join("Sdk");
1233 if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1234 return Ok(sdk_path);
1235 }
1236 }
1237 }
1238
1239 Err(EmulatorError::AndroidHomeNotFound)
1240}
1241
1242pub async fn list_avds() -> Result<Vec<String>> {
1244 let android_home = get_android_home().await?;
1245
1246 tokio::task::spawn_blocking(move || {
1247 let emulator_path = android_home.join("emulator").join("emulator");
1248
1249 if !emulator_path.exists() {
1250 return Err(EmulatorError::EmulatorToolNotFound(
1251 emulator_path.display().to_string(),
1252 ));
1253 }
1254
1255 let output = Command::new(&emulator_path).arg("-list-avds").output()?;
1256
1257 let avds: Vec<String> = String::from_utf8_lossy(&output.stdout)
1258 .lines()
1259 .map(|s| s.trim().to_string())
1260 .filter(|s| !s.is_empty())
1261 .collect();
1262
1263 if avds.is_empty() {
1264 Err(EmulatorError::NoAvdsFound)
1265 } else {
1266 Ok(avds)
1267 }
1268 })
1269 .await
1270 .map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?
1271}
1272
1273fn parse_ini(content: &str) -> std::collections::HashMap<String, String> {
1275 content
1276 .lines()
1277 .filter_map(|line| {
1278 let line = line.trim();
1279 if line.is_empty() || line.starts_with('#') {
1280 return None;
1281 }
1282 line.split_once('=')
1283 .map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
1284 })
1285 .collect()
1286}
1287
1288async fn adb_server() -> Result<adb_client::server::ADBServer> {
1289 use adb_client::server::ADBServer;
1290
1291 let android_home = get_android_home().await?;
1292 let adb_path = android_home.join("platform-tools").join("adb");
1293 let adb_path: String = adb_path
1294 .to_str()
1295 .ok_or_else(|| EmulatorError::AdbError("Invalid Android home path".to_string()))?
1296 .to_string();
1297 let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5037);
1298
1299 tokio::task::spawn_blocking(move || Ok(ADBServer::new_from_path(addr, Some(adb_path))))
1300 .await
1301 .map_err(|e| EmulatorError::AdbError(format!("Task join error: {}", e)))?
1302}
1303
1304pub async fn list_emulators() -> Result<Vec<Emulator>> {
1315 use adb_client::emulator::ADBEmulatorDevice;
1316
1317 let mut server = adb_server().await?;
1318
1319 tokio::task::spawn_blocking(move || {
1320 let mut emulators = vec![];
1321
1322 let devices = server.devices().map_err(|e| {
1323 EmulatorError::IoError(std::io::Error::other(format!(
1324 "Failed to list ADB devices: {}",
1325 e
1326 )))
1327 })?;
1328
1329 for device in devices {
1331 if device.identifier.starts_with("emulator-") {
1332 let mut emulator_device = ADBEmulatorDevice::new(device.identifier.clone(), None)
1334 .map_err(|e| {
1335 EmulatorError::IoError(std::io::Error::other(format!(
1336 "Failed to create ADBEmulatorDevice: {}",
1337 e
1338 )))
1339 })?;
1340
1341 if let Ok(discovery_path) = emulator_device.avd_discovery_path()
1343 && let Ok(ini_content) = std::fs::read_to_string(&discovery_path)
1344 {
1345 let metadata = parse_ini(&ini_content);
1346
1347 if let Some(port_str) = metadata.get("grpc.port")
1348 && let Ok(grpc_port) = port_str.parse::<u16>()
1349 {
1350 emulators.push(Emulator {
1351 is_owned: false,
1352 process: tokio::sync::Mutex::new(None),
1353 grpc_port,
1354 serial: device.identifier.clone(),
1355 metadata,
1356 discovery_path: discovery_path.clone(),
1357 issuer: None,
1358 stdout_thread: tokio::sync::Mutex::new(None),
1359 stderr_thread: tokio::sync::Mutex::new(None),
1360 });
1361 }
1362 }
1363 }
1364 }
1365
1366 Ok(emulators)
1367 })
1368 .await
1369 .map_err(|e| EmulatorError::EnumerationFailed(format!("Task join error: {}", e)))?
1370}
1371
1372pub async fn connect_or_start_emulator(
1383 config: EmulatorConfig,
1384) -> Result<(EmulatorClient, Option<Emulator>)> {
1385 if let Ok(client) = EmulatorClient::connect_avd(config.avd_id()).await {
1387 tracing::info!("Connected to existing emulator");
1388 return Ok((client, None));
1389 }
1390
1391 tracing::info!("No existing emulator found, starting new one...");
1392 let instance = config.spawn().await?;
1394 tracing::info!("Emulator started at: {}", instance.grpc_endpoint());
1395
1396 tracing::info!("Waiting for emulator to be ready...");
1398 let client = instance
1399 .connect(Some(Duration::from_secs(120)), true)
1400 .await?;
1401 tracing::info!("Connected to new emulator");
1402
1403 Ok((client, Some(instance)))
1404}