1use std::io;
7use std::net::{Ipv4Addr, SocketAddrV4};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11use thiserror::Error;
12use tokio::io::{AsyncBufReadExt, BufReader};
13use tokio::process::{Child, Command};
14use tokio::task::JoinHandle;
15
16pub mod auth;
17pub mod proto;
18
19#[cfg(windows)]
20mod windows;
21
22#[cfg(unix)]
23mod unix;
24
25pub use proto::emulator_controller_client::EmulatorControllerClient;
26use tonic::transport::Channel;
27
28use crate::auth::AuthProvider;
29
30#[doc = include_str!("../README.md")]
31#[cfg(doctest)]
32pub struct ReadmeDoctests;
33
34const EMULATOR_BIN: &str = const {
35 if cfg!(windows) {
36 "emulator.exe"
37 } else {
38 "emulator"
39 }
40};
41
42#[derive(Error, Debug)]
43pub enum EmulatorError {
44 #[error(
45 "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"
46 )]
47 AndroidHomeNotFound,
48
49 #[error("No emulator AVDs found")]
50 NoAvdsFound,
51
52 #[error("Failed to spawn or connect to ADB server: {0}")]
53 AdbError(String),
54
55 #[error("Android SDK emulator tool not found at path: {0}")]
56 EmulatorToolNotFound(String),
57
58 #[error("Invalid gRPC endpoint URI: {0}")]
59 InvalidUri(String),
60
61 #[error("Failed to enumerate running emulators: {0}")]
62 EnumerationFailed(String),
63
64 #[error("Failed to start emulator: {0}")]
65 EmulatorStartFailed(String),
66
67 #[error("Failed to kill emulator: {0}")]
68 EmulatorKillFailed(String),
69
70 #[error("Emulator connection timed out")]
71 ConnectionTimeout,
72
73 #[error("Authentication error: {0}")]
74 AuthError(#[from] crate::auth::AuthError),
75
76 #[error("gRPC connection error: {0}")]
77 GrpcError(#[from] tonic::transport::Error),
78
79 #[error("gRPC status error: {0}")]
80 GrpcStatus(#[from] tonic::Status),
81
82 #[error("IO error: {0}")]
83 IoError(#[from] std::io::Error),
84}
85
86pub type Result<T> = std::result::Result<T, EmulatorError>;
87
88async fn find_free_grpc_port() -> Option<u16> {
92 use adb_client::emulator::ADBEmulatorDevice;
93 use std::collections::HashSet;
94
95 let mut server = adb_server().await.ok()?;
96
97 tokio::task::spawn_blocking(move || {
98 let devices = match server.devices() {
99 Ok(d) => d,
100 Err(_) => return None,
101 };
102
103 let mut used_ports = HashSet::new();
105 for device in devices {
106 if device.identifier.starts_with("emulator-")
107 && let Ok(mut emulator_device) = ADBEmulatorDevice::new(device.identifier, None)
108 && let Ok(discovery_path) = emulator_device.avd_discovery_path()
109 && let Ok(ini_content) = std::fs::read_to_string(&discovery_path)
110 {
111 let metadata = parse_ini(&ini_content);
112 if let Some(port_str) = metadata.get("grpc.port")
113 && let Ok(port) = port_str.parse::<u16>()
114 {
115 used_ports.insert(port);
116 }
117 }
118 }
119
120 (8554..8600).find(|&port| !used_ports.contains(&port))
122 })
123 .await
124 .ok()?
125}
126
127fn log_emulator_line(line: &str) {
129 let trimmed = line.trim_start();
130
131 if let Some(rest) = trimmed.strip_prefix("ERROR ") {
132 tracing::error!("{}", rest.trim_start());
133 } else if let Some(rest) = trimmed.strip_prefix("WARNING ") {
134 tracing::warn!("{}", rest.trim_start());
135 } else if let Some(rest) = trimmed.strip_prefix("WARN ") {
136 tracing::warn!("{}", rest.trim_start());
137 } else if let Some(rest) = trimmed.strip_prefix("INFO ") {
138 tracing::info!("{}", rest.trim_start());
139 } else if let Some(rest) = trimmed.strip_prefix("DEBUG ") {
140 tracing::debug!("{}", rest.trim_start());
141 } else if let Some(rest) = trimmed.strip_prefix("TRACE ") {
142 tracing::trace!("{}", rest.trim_start());
143 } else {
144 tracing::debug!("{}", line);
146 }
147}
148
149#[derive(Debug, Clone)]
151pub enum GrpcAuthConfig {
152 None,
154 Basic,
156 Jwt {
158 issuer: Option<String>,
160 },
161}
162
163impl Default for GrpcAuthConfig {
164 fn default() -> Self {
165 GrpcAuthConfig::Jwt { issuer: None }
166 }
167}
168
169#[derive(Debug)]
171pub struct EmulatorConfig {
172 avd_name: String,
174 grpc_port: Option<u16>,
176 grpc_auth: GrpcAuthConfig,
178 no_window: bool,
180 no_snapshot_load: bool,
182 no_snapshot_save: bool,
184 no_boot_anim: bool,
186 no_acceleration: bool,
188 dalvik_vm_check_jni: bool,
190 read_only: bool,
192 quit_after_boot: Option<Duration>,
194 extra_args: Vec<String>,
196 grpc_allowlist: Option<auth::GrpcAllowlist>,
199 stdout: Option<std::process::Stdio>,
201 stderr: Option<std::process::Stdio>,
203}
204
205impl EmulatorConfig {
206 pub fn new(avd_name: impl Into<String>) -> Self {
208 Self {
209 avd_name: avd_name.into(),
210 grpc_port: None,
211 grpc_auth: GrpcAuthConfig::default(),
212 no_window: true,
213 no_snapshot_load: false,
214 no_snapshot_save: false,
215 no_boot_anim: false,
216 no_acceleration: false,
217 dalvik_vm_check_jni: false,
218 read_only: false,
219 quit_after_boot: None,
220 extra_args: Vec::new(),
221 grpc_allowlist: None,
222 stdout: None,
223 stderr: None,
224 }
225 }
226
227 pub fn avd_id(&self) -> &str {
229 &self.avd_name
230 }
231
232 async fn poll_for_emulator(
234 grpc_port: u16,
235 ) -> Result<(String, std::collections::HashMap<String, String>, PathBuf)> {
236 use adb_client::emulator::ADBEmulatorDevice;
237
238 let mut server = adb_server().await?;
239 tokio::task::spawn_blocking(move || {
240 loop {
241 std::thread::sleep(Duration::from_millis(500));
242
243 let devices = match server.devices() {
244 Ok(d) => d,
245 Err(_) => continue,
246 };
247
248 for device in devices {
249 if !device.identifier.starts_with("emulator-") {
250 continue;
251 }
252
253 let mut emulator_device =
254 match ADBEmulatorDevice::new(device.identifier.clone(), None) {
255 Ok(d) => d,
256 Err(_) => continue,
257 };
258
259 let discovery_path = match emulator_device.avd_discovery_path() {
260 Ok(p) => p,
261 Err(_) => continue,
262 };
263
264 let ini_content = match std::fs::read_to_string(&discovery_path) {
265 Ok(c) => c,
266 Err(_) => continue,
267 };
268
269 let metadata = parse_ini(&ini_content);
270
271 if let Some(port_str) = metadata.get("grpc.port")
273 && let Ok(found_port) = port_str.parse::<u16>()
274 && found_port == grpc_port
275 {
276 return Ok((device.identifier, metadata, discovery_path));
277 }
278 }
279 }
280 })
281 .await
282 .map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?
283 }
284
285 pub fn with_grpc_auth(mut self, auth: GrpcAuthConfig) -> Self {
327 self.grpc_auth = auth;
328 self
329 }
330
331 pub fn with_grpc_port(mut self, port: u16) -> Self {
344 self.grpc_port = Some(port);
345 self
346 }
347
348 pub fn with_window(mut self, show: bool) -> Self {
352 self.no_window = !show;
353 self
354 }
355
356 pub fn with_snapshot_load(mut self, load: bool) -> Self {
361 self.no_snapshot_load = !load;
362 self
363 }
364
365 pub fn with_snapshot_save(mut self, save: bool) -> Self {
370 self.no_snapshot_save = !save;
371 self
372 }
373
374 pub fn with_boot_animation(mut self, show: bool) -> Self {
379 self.no_boot_anim = !show;
380 self
381 }
382
383 pub fn with_acceleration(mut self, enable: bool) -> Self {
389 self.no_acceleration = !enable;
390 self
391 }
392
393 pub fn with_dalvik_vm_check_jni(mut self, enable: bool) -> Self {
399 self.dalvik_vm_check_jni = enable;
400 self
401 }
402
403 pub fn with_read_only(mut self, read_only: bool) -> Self {
410 self.read_only = read_only;
411 self
412 }
413
414 pub fn with_quit_after_boot(mut self, duration: Option<Duration>) -> Self {
423 self.quit_after_boot = duration;
424 self
425 }
426
427 pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
428 self.extra_args = args;
429 self
430 }
431
432 pub fn with_grpc_allowlist(mut self, allowlist: auth::GrpcAllowlist) -> Self {
487 self.grpc_allowlist = Some(allowlist);
488 self
489 }
490
491 pub fn stdout<T: Into<std::process::Stdio>>(mut self, cfg: T) -> Self {
493 self.stdout = Some(cfg.into());
494 self
495 }
496
497 pub fn stderr<T: Into<std::process::Stdio>>(mut self, cfg: T) -> Self {
499 self.stderr = Some(cfg.into());
500 self
501 }
502
503 pub async fn spawn(self) -> Result<Emulator> {
505 let android_home = get_android_home().await?;
506 let emulator_path = android_home.join("emulator").join(EMULATOR_BIN);
507
508 if !tokio::fs::try_exists(&emulator_path).await.unwrap_or(false) {
509 return Err(EmulatorError::EmulatorToolNotFound(
510 emulator_path.display().to_string(),
511 ));
512 }
513
514 let mut cmd = Command::new(&emulator_path);
515 cmd.arg("-avd").arg(&self.avd_name);
516
517 if self.no_window {
518 cmd.arg("-no-window");
519 }
520
521 if self.no_snapshot_load {
522 cmd.arg("-no-snapshot-load");
523 }
524
525 if self.no_acceleration {
526 cmd.arg("-accel").arg("off");
527 }
528
529 if self.no_boot_anim {
530 cmd.arg("-no-boot-anim");
531 }
532
533 if self.dalvik_vm_check_jni {
534 cmd.arg("-dalvik-vm-checkjni");
535 }
536
537 if self.read_only {
538 cmd.arg("-read-only");
539 }
540
541 if let Some(quit_after) = self.quit_after_boot {
542 cmd.arg("-quit-after-boot")
543 .arg(quit_after.as_secs().to_string());
544 }
545
546 let use_default_stdout = self.stdout.is_none();
548 let use_default_stderr = self.stderr.is_none();
549
550 if let Some(stdout) = self.stdout {
552 cmd.stdout(stdout);
553 } else {
554 cmd.stdout(std::process::Stdio::piped());
555 }
556
557 if let Some(stderr) = self.stderr {
559 cmd.stderr(stderr);
560 } else {
561 cmd.stderr(std::process::Stdio::piped());
562 }
563
564 cmd.stdin(std::process::Stdio::null());
565
566 let grpc_port = match self.grpc_port {
569 Some(port) => port,
570 None => find_free_grpc_port().await.unwrap_or(8554),
571 };
572 cmd.arg("-grpc").arg(grpc_port.to_string());
573
574 let issuer = match self.grpc_auth {
575 GrpcAuthConfig::None => {
576 None
578 }
579 GrpcAuthConfig::Basic => {
580 cmd.arg("-grpc-use-token");
582 None
583 }
584 GrpcAuthConfig::Jwt { issuer } => {
585 let issuer = issuer.unwrap_or_else(|| format!("emulator-{}", grpc_port));
587
588 let allowlist = self
590 .grpc_allowlist
591 .unwrap_or_else(|| auth::GrpcAllowlist::default_for_issuer(&issuer));
592
593 let allowlist_json = serde_json::to_string_pretty(&allowlist).map_err(|e| {
595 EmulatorError::EmulatorStartFailed(format!(
596 "Failed to serialize allowlist: {}",
597 e
598 ))
599 })?;
600
601 let temp_dir = std::env::temp_dir();
602 let allowlist_path =
603 temp_dir.join(format!("emulator-allowlist-{}.json", std::process::id()));
604 tokio::fs::write(&allowlist_path, allowlist_json).await?;
605
606 cmd.arg("-grpc-allowlist").arg(&allowlist_path);
607
608 cmd.arg("-grpc-use-jwt");
610
611 Some(issuer)
612 }
613 };
614
615 for arg in &self.extra_args {
616 cmd.arg(arg);
617 }
618
619 #[cfg(windows)]
623 let (job, mut process) = crate::windows::EmulatorJob::spawn(cmd)
624 .map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
625
626 #[cfg(unix)]
627 let (process_group, mut process) = crate::unix::EmulatorProcessGroup::spawn(cmd)
628 .map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
629
630 #[cfg(not(any(windows, unix)))]
631 let mut process = cmd
632 .spawn()
633 .map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
634
635 let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
637
638 let stdout_task = if use_default_stdout {
640 let child_out = process.stdout.take().expect("stdout should be piped");
641 let mut shutdown_rx = shutdown_rx.clone();
642
643 let handle = tokio::spawn(async move {
644 tracing::info!("Stdout forwarding task started");
645 let reader = BufReader::new(child_out);
646 let mut lines = reader.lines();
647
648 loop {
649 tokio::select! {
650 res = shutdown_rx.wait_for(|v| *v) => {
651 match res {
652 Ok(_) => tracing::info!("Stdout forwarding task received shutdown signal"),
653 Err(_) => tracing::info!("Stdout forwarding task: shutdown sender dropped"),
654 }
655 break;
656 }
657 result = lines.next_line() => {
658 match result {
659 Ok(Some(line)) => log_emulator_line(&line),
660 Ok(None) => {
661 tracing::debug!("Stdout EOF reached");
662 break;
663 }
664 Err(e) => {
665 tracing::error!("Error reading stdout: {}", e);
666 return Err(e);
667 }
668 }
669 }
670 }
671 }
672
673 tracing::info!("Stdout forwarding task exiting");
674 Ok(())
675 });
676 Some(handle)
677 } else {
678 None
679 };
680
681 let stderr_task = if use_default_stderr {
683 let child_stderr = process.stderr.take().expect("stderr should be piped");
684 let mut shutdown_rx = shutdown_rx.clone();
685
686 let handle = tokio::spawn(async move {
687 tracing::info!("Stderr forwarding task started");
688 let reader = BufReader::new(child_stderr);
689 let mut lines = reader.lines();
690
691 loop {
692 tokio::select! {
693 res = shutdown_rx.wait_for(|v| *v) => {
694 match res {
695 Ok(_) => tracing::info!("Stderr forwarding task received shutdown signal"),
696 Err(_) => tracing::info!("Stderr forwarding task: shutdown sender dropped"),
697 }
698 break;
699 }
700 result = lines.next_line() => {
701 match result {
702 Ok(Some(line)) => log_emulator_line(&line),
703 Ok(None) => {
704 tracing::debug!("Stderr EOF reached");
705 break;
706 }
707 Err(e) => {
708 tracing::error!("Error reading stderr: {}", e);
709 return Err(e);
710 }
711 }
712 }
713 }
714 }
715
716 tracing::info!("Stderr forwarding task exiting");
717 Ok(())
718 });
719 Some(handle)
720 } else {
721 None
722 };
723
724 let (serial, metadata, discovery_path) = Self::poll_for_emulator(grpc_port).await?;
726
727 let owned_process = OwnedProcess {
728 process,
729 #[cfg(windows)]
730 job: Some(job),
731 #[cfg(unix)]
732 process_group: Some(process_group),
733 stdout_task,
734 stderr_task,
735 shutdown_tx: Some(shutdown_tx),
736 };
737
738 Ok(Emulator {
739 owned_process: Some(tokio::sync::Mutex::new(Some(owned_process))),
740 grpc_port,
741 serial,
742 metadata,
743 discovery_path,
744 issuer,
745 })
746 }
747}
748
749pub struct EmulatorClient {
754 provider: auth::AuthProvider,
755 interceptor: EmulatorControllerClient<
756 tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
757 >,
758 endpoint: String,
759}
760
761impl EmulatorClient {
762 pub async fn connect_avd(avd: &str) -> Result<Self> {
764 let emulators = list_emulators().await?;
765 let matching = emulators
766 .into_iter()
767 .find(|e| e.avd_id().map(|id| id == avd).unwrap_or(false));
768 if let Some(emulator) = matching {
769 emulator.connect(Some(Duration::from_secs(30)), true).await
770 } else {
771 Err(EmulatorError::EmulatorStartFailed(
772 "No running emulator found".to_string(),
773 ))
774 }
775 }
776
777 pub async fn connect(endpoint: impl Into<String>) -> Result<Self> {
779 let endpoint = endpoint.into();
780 let channel = Channel::from_shared(endpoint.clone())
781 .map_err(|e| EmulatorError::InvalidUri(e.to_string()))?
782 .connect()
783 .await?;
784
785 let provider = std::sync::Arc::new(auth::NoOpTokenProvider);
786 let provider = auth::AuthProvider::new_with_token_provider(provider);
787
788 Ok(Self {
789 interceptor: EmulatorControllerClient::with_interceptor(channel, provider.clone()),
790 provider,
791 endpoint,
792 })
793 }
794
795 pub async fn connect_with_auth(
797 endpoint: impl Into<String>,
798 provider: auth::AuthProvider,
799 ) -> Result<Self> {
800 let endpoint = endpoint.into();
801 let channel = Channel::from_shared(endpoint.clone())
802 .map_err(|e| EmulatorError::InvalidUri(e.to_string()))?
803 .connect()
804 .await?;
805
806 Ok(Self {
807 interceptor: EmulatorControllerClient::with_interceptor(channel, provider.clone()),
808 provider,
809 endpoint,
810 })
811 }
812
813 pub fn auth_scheme(&self) -> &auth::AuthScheme {
815 self.provider.auth_scheme()
816 }
817
818 pub fn export_token(&self, auds: &[&str], ttl: Duration) -> Result<auth::BearerToken> {
830 let token = self.provider.export_token(auds, ttl)?;
831 Ok(token)
832 }
833
834 pub fn endpoint(&self) -> &str {
836 &self.endpoint
837 }
838
839 pub fn protocol_mut(
841 &mut self,
842 ) -> &mut EmulatorControllerClient<
843 tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
844 > {
845 &mut self.interceptor
846 }
847
848 pub fn protocol(
850 &self,
851 ) -> &EmulatorControllerClient<
852 tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
853 > {
854 &self.interceptor
855 }
856
857 pub async fn wait_until_booted(
893 &mut self,
894 timeout: Duration,
895 poll_interval: Option<Duration>,
896 ) -> Result<Duration> {
897 let poll_interval = poll_interval.unwrap_or(Duration::from_secs(2));
898 let start = std::time::Instant::now();
899 let mut attempt = 0;
900
901 loop {
902 attempt += 1;
903 let status = self.protocol_mut().get_status(()).await?.into_inner();
904
905 if status.booted {
906 let elapsed = start.elapsed();
907 tracing::info!(
908 "Emulator fully booted after {:.1} seconds ({} attempts)",
909 elapsed.as_secs_f64(),
910 attempt
911 );
912 return Ok(elapsed);
913 }
914
915 tracing::debug!(
916 "Boot status: {} (attempt {}, elapsed: {:.1}s)",
917 status.booted,
918 attempt,
919 start.elapsed().as_secs_f64()
920 );
921
922 if start.elapsed() >= timeout {
924 return Err(EmulatorError::ConnectionTimeout);
925 }
926
927 let remaining = timeout.saturating_sub(start.elapsed());
929 let sleep_duration = poll_interval.min(remaining);
930
931 if sleep_duration.is_zero() {
932 return Err(EmulatorError::ConnectionTimeout);
933 }
934
935 tokio::time::sleep(sleep_duration).await;
936 }
937 }
938
939 pub async fn shutdown(&mut self, timeout: Option<Duration>) -> Result<()> {
994 use crate::proto::{VmRunState, vm_run_state::RunState};
995
996 let timeout = timeout.unwrap_or(Duration::from_secs(30));
997 let poll_interval = Duration::from_millis(500);
998
999 tracing::info!("Requesting graceful emulator shutdown...");
1000
1001 let shutdown_state = VmRunState {
1003 state: RunState::Shutdown as i32,
1004 };
1005
1006 self.protocol_mut()
1007 .set_vm_state(shutdown_state)
1008 .await
1009 .map_err(|e| {
1010 EmulatorError::EmulatorKillFailed(format!(
1011 "Failed to set VM state to SHUTDOWN: {}",
1012 e
1013 ))
1014 })?;
1015
1016 tracing::info!("Shutdown request sent, waiting for VM to shut down...");
1017
1018 let start = std::time::Instant::now();
1020 let mut last_state = None;
1021
1022 loop {
1023 match self.protocol_mut().get_vm_state(()).await {
1024 Ok(response) => {
1025 let vm_state = response.into_inner();
1026 let state = RunState::try_from(vm_state.state).unwrap_or(RunState::Unknown);
1027
1028 if last_state != Some(state) {
1029 tracing::debug!("VM state: {:?}", state);
1030 last_state = Some(state);
1031 }
1032
1033 match state {
1036 RunState::Unknown => {
1037 tracing::info!("VM entered unknown state, proceeding with termination");
1039 break;
1040 }
1041 _ => {
1042 }
1044 }
1045 }
1046 Err(e) => {
1047 tracing::info!(
1049 "Lost connection to emulator ({}), assuming shutdown complete",
1050 e
1051 );
1052 break;
1053 }
1054 }
1055
1056 if start.elapsed() >= timeout {
1058 tracing::warn!(
1059 "Shutdown timeout reached after {:.1} seconds, forcing termination",
1060 timeout.as_secs_f64()
1061 );
1062 break;
1063 }
1064
1065 let remaining = timeout.saturating_sub(start.elapsed());
1067 let sleep_duration = poll_interval.min(remaining);
1068
1069 if sleep_duration.is_zero() {
1070 break;
1071 }
1072
1073 tokio::time::sleep(sleep_duration).await;
1074 }
1075
1076 tracing::info!("VM shutdown complete");
1077 Ok(())
1078 }
1079}
1080
1081#[derive(Debug)]
1087struct OwnedProcess {
1088 process: Child,
1090 #[cfg(windows)]
1092 job: Option<crate::windows::EmulatorJob>,
1093 #[cfg(unix)]
1095 process_group: Option<crate::unix::EmulatorProcessGroup>,
1096 stdout_task: Option<JoinHandle<io::Result<()>>>,
1098 stderr_task: Option<JoinHandle<io::Result<()>>>,
1100 shutdown_tx: Option<tokio::sync::watch::Sender<bool>>,
1102}
1103
1104async fn kill_owned_process(mut owned_process: OwnedProcess) -> Result<()> {
1109 let pid = owned_process.process.id();
1110
1111 if let Some(pid) = pid {
1113 tracing::info!("Terminating emulator process with PID {}", pid);
1114 }
1115
1116 #[cfg(windows)]
1119 {
1120 if let Some(job) = &owned_process.job {
1121 if let Err(err) = job.kill() {
1122 tracing::error!("Failed to kill emulator job: {}", err);
1123 return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1124 }
1125 } else {
1126 if let Err(err) = owned_process.process.start_kill() {
1128 tracing::error!("Failed to kill emulator process: {}", err);
1129 return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1130 }
1131 }
1132 }
1133
1134 #[cfg(unix)]
1137 {
1138 if let Some(process_group) = &owned_process.process_group {
1139 if let Err(err) = process_group.kill() {
1140 tracing::error!("Failed to kill emulator process group: {}", err);
1141 return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1142 }
1143 } else {
1144 if let Err(err) = owned_process.process.start_kill() {
1146 tracing::error!("Failed to kill emulator process: {}", err);
1147 return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1148 }
1149 }
1150 }
1151
1152 #[cfg(not(any(windows, unix)))]
1154 if let Err(err) = owned_process.process.start_kill() {
1155 tracing::error!("Failed to kill emulator process: {}", err);
1156 return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
1157 }
1158
1159 let wait_res = match owned_process.process.wait().await {
1165 Ok(status) => {
1166 if let Some(pid) = pid {
1167 tracing::info!(
1168 "Emulator process with PID {} has exited with status: {:?}",
1169 pid,
1170 status
1171 );
1172 }
1173 Ok(())
1174 }
1175 Err(err) => {
1176 tracing::error!("Failed to wait for emulator process to exit: {}", err);
1177 Err(EmulatorError::EmulatorKillFailed(err.to_string()))
1178 }
1179 };
1180
1181 if let Some(tx) = &owned_process.shutdown_tx {
1183 tracing::info!("Sending shutdown signal to IO forwarding tasks");
1184 let _ = tx.send(true);
1185 }
1186
1187 if let Some(stdout_task) = owned_process.stdout_task.take() {
1189 tracing::info!("Joining stdout forwarding task...");
1190 match stdout_task.await {
1191 Ok(Ok(())) => {
1192 tracing::info!("Stdout forwarding task completed successfully")
1193 }
1194 Ok(Err(e)) => {
1195 tracing::warn!("Stdout forwarding task completed with error: {}", e)
1196 }
1197 Err(e) => {
1198 if e.is_cancelled() {
1199 tracing::debug!("Stdout forwarding task was cancelled");
1200 } else {
1201 tracing::error!("Failed to join stdout forwarding task: {}", e);
1202 }
1203 }
1204 }
1205 }
1206
1207 if let Some(stderr_task) = owned_process.stderr_task.take() {
1208 tracing::info!("Joining stderr forwarding task...");
1209 match stderr_task.await {
1210 Ok(Ok(())) => {
1211 tracing::info!("Stderr forwarding task completed successfully")
1212 }
1213 Ok(Err(e)) => {
1214 tracing::warn!("Stderr forwarding task completed with error: {}", e)
1215 }
1216 Err(e) => {
1217 if e.is_cancelled() {
1218 tracing::debug!("Stderr forwarding task was cancelled");
1219 } else {
1220 tracing::error!("Failed to join stderr forwarding task: {}", e);
1221 }
1222 }
1223 }
1224 }
1225
1226 wait_res
1227}
1228
1229#[derive(Debug)]
1234pub struct Emulator {
1235 owned_process: Option<tokio::sync::Mutex<Option<OwnedProcess>>>,
1240 serial: String,
1241 grpc_port: u16,
1242 discovery_path: PathBuf,
1244 metadata: std::collections::HashMap<String, String>,
1246 issuer: Option<String>,
1248}
1249
1250impl Emulator {
1251 pub fn serial(&self) -> &str {
1253 &self.serial
1254 }
1255
1256 pub fn is_owned(&self) -> bool {
1258 self.owned_process.is_some()
1259 }
1260
1261 pub fn discovery_path(&self) -> &Path {
1262 self.discovery_path.as_path()
1263 }
1264
1265 pub fn metadata(&self) -> &std::collections::HashMap<String, String> {
1267 &self.metadata
1268 }
1269
1270 pub fn get_metadata(&self, key: &str) -> Option<&str> {
1272 self.metadata.get(key).map(|s| s.as_str())
1273 }
1274
1275 pub fn requires_jwt_auth(&self) -> bool {
1277 self.get_metadata("grpc.jwk_active").is_some()
1278 }
1279
1280 pub fn avd_name(&self) -> Option<&str> {
1282 self.get_metadata("avd.name")
1283 }
1284
1285 pub fn avd_id(&self) -> Option<&str> {
1287 self.get_metadata("avd.id")
1288 }
1289
1290 pub fn avd_dir(&self) -> Option<&str> {
1292 self.get_metadata("avd.dir")
1293 }
1294
1295 pub fn emulator_version(&self) -> Option<&str> {
1297 self.get_metadata("emulator.version")
1298 }
1299
1300 pub fn emulator_build(&self) -> Option<&str> {
1302 self.get_metadata("emulator.build")
1303 }
1304
1305 pub fn port_serial(&self) -> Option<u16> {
1307 self.get_metadata("port.serial")?.parse().ok()
1308 }
1309
1310 pub fn port_adb(&self) -> Option<u16> {
1312 self.get_metadata("port.adb")?.parse().ok()
1313 }
1314
1315 pub fn cmdline(&self) -> Option<&str> {
1317 self.get_metadata("cmdline")
1318 }
1319
1320 pub fn grpc_endpoint(&self) -> String {
1322 format!("http://localhost:{}", self.grpc_port)
1323 }
1324
1325 pub fn grpc_port(&self) -> u16 {
1327 self.grpc_port
1328 }
1329
1330 pub async fn connect(
1345 &self,
1346 timeout: Option<Duration>,
1347 allow_basic_auth: bool,
1348 ) -> Result<EmulatorClient> {
1349 let basic_auth_token = self.get_metadata("grpc.token");
1351
1352 if self.requires_jwt_auth() {
1353 tracing::info!(
1354 "Emulator requires JWT authentication, setting up ES256 token provider..."
1355 );
1356
1357 match self.connect_with_jwt_auth(timeout).await {
1359 Ok(client) => {
1360 tracing::info!("Connected to emulator with JWT authentication.");
1361 return Ok(client);
1362 }
1363 Err(err) => {
1364 tracing::error!("Failed to connect with JWT authentication: {}", err);
1365 if basic_auth_token.is_some() && allow_basic_auth {
1366 tracing::warn!("Falling back to basic authentication...");
1367 } else {
1368 return Err(err);
1369 }
1370 }
1371 }
1372 } else {
1373 tracing::info!("Emulator does not require JWT authentication.");
1374 }
1375
1376 if allow_basic_auth && let Some(token) = basic_auth_token {
1379 tracing::info!("Emulator accepts basic auth, setting up BasicAuthTokenProvider...");
1380 return self.connect_with_basic_auth(token, timeout).await;
1381 }
1382
1383 self.connect_with_noop_auth(timeout).await
1385 }
1386
1387 async fn connect_with_noop_auth(&self, timeout: Option<Duration>) -> Result<EmulatorClient> {
1389 let start = std::time::Instant::now();
1390
1391 let provider = Arc::new(auth::NoOpTokenProvider);
1392 let provider = AuthProvider::new_with_token_provider(provider);
1393 loop {
1394 match EmulatorClient::connect_with_auth(self.grpc_endpoint(), provider.clone()).await {
1395 Ok(mut client) => {
1396 if client.protocol_mut().get_status(()).await.is_ok() {
1398 return Ok(client);
1399 }
1400 }
1401 Err(err) => {
1402 tracing::error!("No-auth connection attempt failed: {}", err);
1403 }
1404 }
1405
1406 if let Some(timeout_duration) = timeout
1408 && start.elapsed() > timeout_duration
1409 {
1410 return Err(EmulatorError::ConnectionTimeout);
1411 }
1412
1413 tokio::time::sleep(Duration::from_secs(1)).await;
1414 }
1415 }
1416
1417 async fn connect_with_basic_auth(
1419 &self,
1420 token: &str,
1421 timeout: Option<Duration>,
1422 ) -> Result<EmulatorClient> {
1423 let start = std::time::Instant::now();
1424
1425 let provider = Arc::new(auth::BearerTokenProvider::new(token.to_string()));
1426 let provider = AuthProvider::new_with_token_provider(provider);
1427
1428 loop {
1429 match EmulatorClient::connect_with_auth(self.grpc_endpoint(), provider.clone()).await {
1430 Ok(mut client) => {
1431 if client.protocol_mut().get_status(()).await.is_ok() {
1433 return Ok(client);
1434 }
1435 }
1436 Err(err) => {
1437 tracing::error!("Basic auth connection attempt failed: {}", err);
1438 }
1439 }
1440
1441 if let Some(timeout_duration) = timeout
1443 && start.elapsed() > timeout_duration
1444 {
1445 return Err(EmulatorError::ConnectionTimeout);
1446 }
1447
1448 tokio::time::sleep(Duration::from_secs(1)).await;
1449 }
1450 }
1451
1452 async fn connect_with_jwt_auth(&self, timeout: Option<Duration>) -> Result<EmulatorClient> {
1457 let jwks_path = self.get_metadata("grpc.jwks").ok_or_else(|| {
1459 EmulatorError::EmulatorStartFailed(
1460 "Emulator requires JWT auth but grpc.jwks path not found in metadata".to_string(),
1461 )
1462 })?;
1463
1464 let jwks_dir = PathBuf::from(jwks_path);
1465
1466 let issuer = self
1467 .issuer
1468 .as_deref()
1469 .unwrap_or("android-studio")
1470 .to_string();
1471
1472 let jwt_provider = tokio::task::spawn_blocking(
1474 move || -> std::result::Result<_, crate::auth::AuthError> {
1475 tracing::info!(
1476 "Generating and registering JWT token provider with issuer '{}'",
1477 issuer
1478 );
1479 let provider = auth::JwtTokenProvider::new_and_register(&jwks_dir, issuer)?;
1480
1481 tracing::info!("JWT token provider registered, waiting for activation...");
1482 provider.wait_for_activation(&jwks_dir, Duration::from_secs(10))?;
1484
1485 let provider = AuthProvider::new_with_token_provider(provider);
1486
1487 Ok(provider)
1488 },
1489 )
1490 .await
1491 .map_err(|err| {
1492 EmulatorError::EmulatorStartFailed(format!(
1493 "Failure running task to register JWT token provider: {err}"
1494 ))
1495 })??;
1496
1497 let start = std::time::Instant::now();
1498
1499 loop {
1500 tracing::info!("Attempting JWT connection...");
1501 match EmulatorClient::connect_with_auth(self.grpc_endpoint(), jwt_provider.clone())
1502 .await
1503 {
1504 Ok(mut client) => {
1505 tracing::info!("JWT authentication successful.");
1506 match client.protocol_mut().get_status(()).await {
1508 Ok(_) => {
1509 tracing::info!(
1510 "Successfully connected to emulator with JWT authentication."
1511 );
1512 return Ok(client);
1513 }
1514 Err(err) => {
1515 tracing::error!(
1516 "Failed to get status with JWT authentication: {}",
1517 err
1518 );
1519 }
1520 }
1521 }
1522 Err(err) => {
1523 tracing::error!("JWT connection attempt failed: {}", err);
1524 }
1525 }
1526
1527 if let Some(timeout_duration) = timeout
1529 && start.elapsed() > timeout_duration
1530 {
1531 return Err(EmulatorError::ConnectionTimeout);
1532 }
1533 tracing::info!("Sleeping before retrying JWT connection...");
1534 tokio::time::sleep(Duration::from_secs(1)).await;
1535 }
1536 }
1537
1538 pub async fn kill(&self) -> Result<()> {
1548 let Some(mutex) = &self.owned_process else {
1550 tracing::warn!("kill() called on an emulator that is not owned by this instance");
1551 return Ok(());
1552 };
1553
1554 let owned = mutex.lock().await.take();
1556
1557 if let Some(owned_process) = owned {
1558 kill_owned_process(owned_process).await?;
1559 tracing::info!("Emulator killed successfully");
1560 Ok(())
1561 } else {
1562 tracing::warn!("kill() called but process was already killed");
1563 Ok(())
1564 }
1565 }
1566}
1567
1568impl Drop for Emulator {
1569 fn drop(&mut self) {
1570 if let Some(mutex) = &mut self.owned_process
1574 && let Some(owned_process) = mutex.get_mut().take()
1575 {
1576 tokio::task::spawn(async move {
1579 if let Err(e) = kill_owned_process(owned_process).await {
1580 tracing::error!("Failed to kill emulator in Drop: {}", e);
1581 }
1582 });
1583 }
1584 }
1585}
1586
1587pub async fn get_android_home() -> Result<PathBuf> {
1597 if let Ok(path) = std::env::var("ANDROID_HOME") {
1599 return Ok(PathBuf::from(path));
1600 }
1601
1602 if let Ok(path) = std::env::var("ANDROID_SDK_ROOT") {
1603 return Ok(PathBuf::from(path));
1604 }
1605
1606 #[cfg(target_os = "linux")]
1608 {
1609 if let Some(home) = dirs::home_dir() {
1610 let sdk_path = home.join("Android").join("sdk");
1612 if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1613 return Ok(sdk_path);
1614 }
1615
1616 let sdk_path = home.join("Android").join("Sdk");
1618 if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1619 return Ok(sdk_path);
1620 }
1621 }
1622 }
1623
1624 #[cfg(target_os = "macos")]
1625 {
1626 if let Some(home) = dirs::home_dir() {
1627 let sdk_path = home.join("Library").join("Android").join("sdk");
1628 if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1629 return Ok(sdk_path);
1630 }
1631 }
1632 }
1633
1634 #[cfg(target_os = "windows")]
1635 {
1636 if let Some(local_data) = dirs::data_local_dir() {
1637 let sdk_path = local_data.join("Android").join("Sdk");
1638 if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
1639 return Ok(sdk_path);
1640 }
1641 }
1642 }
1643
1644 Err(EmulatorError::AndroidHomeNotFound)
1645}
1646
1647pub async fn list_avds() -> Result<Vec<String>> {
1649 let android_home = get_android_home().await?;
1650
1651 tokio::task::spawn_blocking(move || {
1652 let emulator_path = android_home.join("emulator").join(EMULATOR_BIN);
1653
1654 if !emulator_path.exists() {
1655 return Err(EmulatorError::EmulatorToolNotFound(
1656 emulator_path.display().to_string(),
1657 ));
1658 }
1659
1660 let output = std::process::Command::new(&emulator_path)
1661 .arg("-list-avds")
1662 .output()?;
1663
1664 let avds: Vec<String> = String::from_utf8_lossy(&output.stdout)
1665 .lines()
1666 .map(|s| s.trim().to_string())
1667 .filter(|s| !s.is_empty())
1668 .collect();
1669
1670 if avds.is_empty() {
1671 Err(EmulatorError::NoAvdsFound)
1672 } else {
1673 Ok(avds)
1674 }
1675 })
1676 .await
1677 .map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?
1678}
1679
1680fn parse_ini(content: &str) -> std::collections::HashMap<String, String> {
1682 content
1683 .lines()
1684 .filter_map(|line| {
1685 let line = line.trim();
1686 if line.is_empty() || line.starts_with('#') {
1687 return None;
1688 }
1689 line.split_once('=')
1690 .map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
1691 })
1692 .collect()
1693}
1694
1695async fn adb_server() -> Result<adb_client::server::ADBServer> {
1696 use adb_client::server::ADBServer;
1697
1698 let android_home = get_android_home().await?;
1699 let adb_path = android_home.join("platform-tools").join("adb");
1700 let adb_path: String = adb_path
1701 .to_str()
1702 .ok_or_else(|| EmulatorError::AdbError("Invalid Android home path".to_string()))?
1703 .to_string();
1704 let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5037);
1705
1706 tokio::task::spawn_blocking(move || Ok(ADBServer::new_from_path(addr, Some(adb_path))))
1707 .await
1708 .map_err(|e| EmulatorError::AdbError(format!("Task join error: {}", e)))?
1709}
1710
1711pub async fn list_emulators() -> Result<Vec<Emulator>> {
1722 use adb_client::emulator::ADBEmulatorDevice;
1723
1724 let mut server = adb_server().await?;
1725
1726 tokio::task::spawn_blocking(move || {
1727 let mut emulators = vec![];
1728
1729 let devices = server.devices().map_err(|e| {
1730 EmulatorError::IoError(std::io::Error::other(format!(
1731 "Failed to list ADB devices: {}",
1732 e
1733 )))
1734 })?;
1735
1736 for device in devices {
1738 if device.identifier.starts_with("emulator-") {
1739 let mut emulator_device = ADBEmulatorDevice::new(device.identifier.clone(), None)
1741 .map_err(|e| {
1742 EmulatorError::IoError(std::io::Error::other(format!(
1743 "Failed to create ADBEmulatorDevice: {}",
1744 e
1745 )))
1746 })?;
1747
1748 if let Ok(discovery_path) = emulator_device.avd_discovery_path()
1750 && let Ok(ini_content) = std::fs::read_to_string(&discovery_path)
1751 {
1752 let metadata = parse_ini(&ini_content);
1753
1754 if let Some(port_str) = metadata.get("grpc.port")
1755 && let Ok(grpc_port) = port_str.parse::<u16>()
1756 {
1757 emulators.push(Emulator {
1758 owned_process: None,
1759 grpc_port,
1760 serial: device.identifier.clone(),
1761 metadata,
1762 discovery_path: discovery_path.clone(),
1763 issuer: None,
1764 });
1765 }
1766 }
1767 }
1768 }
1769
1770 Ok(emulators)
1771 })
1772 .await
1773 .map_err(|e| EmulatorError::EnumerationFailed(format!("Task join error: {}", e)))?
1774}
1775
1776pub async fn connect_or_start_emulator(
1787 config: EmulatorConfig,
1788) -> Result<(EmulatorClient, Option<Emulator>)> {
1789 if let Ok(client) = EmulatorClient::connect_avd(config.avd_id()).await {
1791 tracing::info!("Connected to existing emulator");
1792 return Ok((client, None));
1793 }
1794
1795 tracing::info!("No existing emulator found, starting new one...");
1796 let instance = config.spawn().await?;
1798 tracing::info!("Emulator started at: {}", instance.grpc_endpoint());
1799
1800 tracing::info!("Waiting for emulator to be ready...");
1802 let client = instance
1803 .connect(Some(Duration::from_secs(120)), true)
1804 .await?;
1805 tracing::info!("Connected to new emulator");
1806
1807 Ok((client, Some(instance)))
1808}