use std::io;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::{Child, Command};
use tokio::task::JoinHandle;
pub mod auth;
pub mod proto;
#[cfg(windows)]
mod windows;
#[cfg(unix)]
mod unix;
pub use proto::emulator_controller_client::EmulatorControllerClient;
use tonic::transport::Channel;
use crate::auth::AuthProvider;
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;
const EMULATOR_BIN: &str = const {
if cfg!(windows) {
"emulator.exe"
} else {
"emulator"
}
};
#[derive(Error, Debug)]
pub enum EmulatorError {
#[error(
"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"
)]
AndroidHomeNotFound,
#[error("No emulator AVDs found")]
NoAvdsFound,
#[error("Failed to spawn or connect to ADB server: {0}")]
AdbError(String),
#[error("Android SDK emulator tool not found at path: {0}")]
EmulatorToolNotFound(String),
#[error("Invalid gRPC endpoint URI: {0}")]
InvalidUri(String),
#[error("Failed to enumerate running emulators: {0}")]
EnumerationFailed(String),
#[error("Failed to start emulator: {0}")]
EmulatorStartFailed(String),
#[error("Failed to kill emulator: {0}")]
EmulatorKillFailed(String),
#[error("Emulator connection timed out")]
ConnectionTimeout,
#[error("Authentication error: {0}")]
AuthError(#[from] crate::auth::AuthError),
#[error("gRPC connection error: {0}")]
GrpcError(#[from] tonic::transport::Error),
#[error("gRPC status error: {0}")]
GrpcStatus(#[from] tonic::Status),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, EmulatorError>;
async fn find_free_grpc_port() -> Option<u16> {
use adb_client::emulator::ADBEmulatorDevice;
use std::collections::HashSet;
let mut server = adb_server().await.ok()?;
tokio::task::spawn_blocking(move || {
let devices = match server.devices() {
Ok(d) => d,
Err(_) => return None,
};
let mut used_ports = HashSet::new();
for device in devices {
if device.identifier.starts_with("emulator-")
&& let Ok(mut emulator_device) = ADBEmulatorDevice::new(device.identifier, None)
&& let Ok(discovery_path) = emulator_device.avd_discovery_path()
&& let Ok(ini_content) = std::fs::read_to_string(&discovery_path)
{
let metadata = parse_ini(&ini_content);
if let Some(port_str) = metadata.get("grpc.port")
&& let Ok(port) = port_str.parse::<u16>()
{
used_ports.insert(port);
}
}
}
(8554..8600).find(|&port| !used_ports.contains(&port))
})
.await
.ok()?
}
fn log_emulator_line(line: &str) {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("ERROR ") {
tracing::error!("{}", rest.trim_start());
} else if let Some(rest) = trimmed.strip_prefix("WARNING ") {
tracing::warn!("{}", rest.trim_start());
} else if let Some(rest) = trimmed.strip_prefix("WARN ") {
tracing::warn!("{}", rest.trim_start());
} else if let Some(rest) = trimmed.strip_prefix("INFO ") {
tracing::info!("{}", rest.trim_start());
} else if let Some(rest) = trimmed.strip_prefix("DEBUG ") {
tracing::debug!("{}", rest.trim_start());
} else if let Some(rest) = trimmed.strip_prefix("TRACE ") {
tracing::trace!("{}", rest.trim_start());
} else {
tracing::debug!("{}", line);
}
}
#[derive(Debug, Clone)]
pub enum GrpcAuthConfig {
None,
Basic,
Jwt {
issuer: Option<String>,
},
}
impl Default for GrpcAuthConfig {
fn default() -> Self {
GrpcAuthConfig::Jwt { issuer: None }
}
}
#[derive(Debug)]
pub struct EmulatorConfig {
avd_name: String,
grpc_port: Option<u16>,
grpc_auth: GrpcAuthConfig,
no_window: bool,
no_snapshot_load: bool,
no_snapshot_save: bool,
no_boot_anim: bool,
no_acceleration: bool,
dalvik_vm_check_jni: bool,
read_only: bool,
quit_after_boot: Option<Duration>,
extra_args: Vec<String>,
grpc_allowlist: Option<auth::GrpcAllowlist>,
stdout: Option<std::process::Stdio>,
stderr: Option<std::process::Stdio>,
}
impl EmulatorConfig {
pub fn new(avd_name: impl Into<String>) -> Self {
Self {
avd_name: avd_name.into(),
grpc_port: None,
grpc_auth: GrpcAuthConfig::default(),
no_window: true,
no_snapshot_load: false,
no_snapshot_save: false,
no_boot_anim: false,
no_acceleration: false,
dalvik_vm_check_jni: false,
read_only: false,
quit_after_boot: None,
extra_args: Vec::new(),
grpc_allowlist: None,
stdout: None,
stderr: None,
}
}
pub fn avd_id(&self) -> &str {
&self.avd_name
}
async fn poll_for_emulator(
grpc_port: u16,
) -> Result<(String, std::collections::HashMap<String, String>, PathBuf)> {
use adb_client::emulator::ADBEmulatorDevice;
let mut server = adb_server().await?;
tokio::task::spawn_blocking(move || {
loop {
std::thread::sleep(Duration::from_millis(500));
let devices = match server.devices() {
Ok(d) => d,
Err(_) => continue,
};
for device in devices {
if !device.identifier.starts_with("emulator-") {
continue;
}
let mut emulator_device =
match ADBEmulatorDevice::new(device.identifier.clone(), None) {
Ok(d) => d,
Err(_) => continue,
};
let discovery_path = match emulator_device.avd_discovery_path() {
Ok(p) => p,
Err(_) => continue,
};
let ini_content = match std::fs::read_to_string(&discovery_path) {
Ok(c) => c,
Err(_) => continue,
};
let metadata = parse_ini(&ini_content);
if let Some(port_str) = metadata.get("grpc.port")
&& let Ok(found_port) = port_str.parse::<u16>()
&& found_port == grpc_port
{
return Ok((device.identifier, metadata, discovery_path));
}
}
}
})
.await
.map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?
}
pub fn with_grpc_auth(mut self, auth: GrpcAuthConfig) -> Self {
self.grpc_auth = auth;
self
}
pub fn with_grpc_port(mut self, port: u16) -> Self {
self.grpc_port = Some(port);
self
}
pub fn with_window(mut self, show: bool) -> Self {
self.no_window = !show;
self
}
pub fn with_snapshot_load(mut self, load: bool) -> Self {
self.no_snapshot_load = !load;
self
}
pub fn with_snapshot_save(mut self, save: bool) -> Self {
self.no_snapshot_save = !save;
self
}
pub fn with_boot_animation(mut self, show: bool) -> Self {
self.no_boot_anim = !show;
self
}
pub fn with_acceleration(mut self, enable: bool) -> Self {
self.no_acceleration = !enable;
self
}
pub fn with_dalvik_vm_check_jni(mut self, enable: bool) -> Self {
self.dalvik_vm_check_jni = enable;
self
}
pub fn with_read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
pub fn with_quit_after_boot(mut self, duration: Option<Duration>) -> Self {
self.quit_after_boot = duration;
self
}
pub fn with_extra_args(mut self, args: Vec<String>) -> Self {
self.extra_args = args;
self
}
pub fn with_grpc_allowlist(mut self, allowlist: auth::GrpcAllowlist) -> Self {
self.grpc_allowlist = Some(allowlist);
self
}
pub fn stdout<T: Into<std::process::Stdio>>(mut self, cfg: T) -> Self {
self.stdout = Some(cfg.into());
self
}
pub fn stderr<T: Into<std::process::Stdio>>(mut self, cfg: T) -> Self {
self.stderr = Some(cfg.into());
self
}
pub async fn spawn(self) -> Result<Emulator> {
let android_home = get_android_home().await?;
let emulator_path = android_home.join("emulator").join(EMULATOR_BIN);
if !tokio::fs::try_exists(&emulator_path).await.unwrap_or(false) {
return Err(EmulatorError::EmulatorToolNotFound(
emulator_path.display().to_string(),
));
}
let mut cmd = Command::new(&emulator_path);
cmd.arg("-avd").arg(&self.avd_name);
if self.no_window {
cmd.arg("-no-window");
}
if self.no_snapshot_load {
cmd.arg("-no-snapshot-load");
}
if self.no_acceleration {
cmd.arg("-accel").arg("off");
}
if self.no_boot_anim {
cmd.arg("-no-boot-anim");
}
if self.dalvik_vm_check_jni {
cmd.arg("-dalvik-vm-checkjni");
}
if self.read_only {
cmd.arg("-read-only");
}
if let Some(quit_after) = self.quit_after_boot {
cmd.arg("-quit-after-boot")
.arg(quit_after.as_secs().to_string());
}
let use_default_stdout = self.stdout.is_none();
let use_default_stderr = self.stderr.is_none();
if let Some(stdout) = self.stdout {
cmd.stdout(stdout);
} else {
cmd.stdout(std::process::Stdio::piped());
}
if let Some(stderr) = self.stderr {
cmd.stderr(stderr);
} else {
cmd.stderr(std::process::Stdio::piped());
}
cmd.stdin(std::process::Stdio::null());
let grpc_port = match self.grpc_port {
Some(port) => port,
None => find_free_grpc_port().await.unwrap_or(8554),
};
cmd.arg("-grpc").arg(grpc_port.to_string());
let issuer = match self.grpc_auth {
GrpcAuthConfig::None => {
None
}
GrpcAuthConfig::Basic => {
cmd.arg("-grpc-use-token");
None
}
GrpcAuthConfig::Jwt { issuer } => {
let issuer = issuer.unwrap_or_else(|| format!("emulator-{}", grpc_port));
let allowlist = self
.grpc_allowlist
.unwrap_or_else(|| auth::GrpcAllowlist::default_for_issuer(&issuer));
let allowlist_json = serde_json::to_string_pretty(&allowlist).map_err(|e| {
EmulatorError::EmulatorStartFailed(format!(
"Failed to serialize allowlist: {}",
e
))
})?;
let temp_dir = std::env::temp_dir();
let allowlist_path =
temp_dir.join(format!("emulator-allowlist-{}.json", std::process::id()));
tokio::fs::write(&allowlist_path, allowlist_json).await?;
cmd.arg("-grpc-allowlist").arg(&allowlist_path);
cmd.arg("-grpc-use-jwt");
Some(issuer)
}
};
for arg in &self.extra_args {
cmd.arg(arg);
}
#[cfg(windows)]
let (job, mut process) = crate::windows::EmulatorJob::spawn(cmd)
.map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
#[cfg(unix)]
let (process_group, mut process) = crate::unix::EmulatorProcessGroup::spawn(cmd)
.map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
#[cfg(not(any(windows, unix)))]
let mut process = cmd
.spawn()
.map_err(|e| EmulatorError::EmulatorStartFailed(e.to_string()))?;
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
let stdout_task = if use_default_stdout {
let child_out = process.stdout.take().expect("stdout should be piped");
let mut shutdown_rx = shutdown_rx.clone();
let handle = tokio::spawn(async move {
tracing::info!("Stdout forwarding task started");
let reader = BufReader::new(child_out);
let mut lines = reader.lines();
loop {
tokio::select! {
res = shutdown_rx.wait_for(|v| *v) => {
match res {
Ok(_) => tracing::info!("Stdout forwarding task received shutdown signal"),
Err(_) => tracing::info!("Stdout forwarding task: shutdown sender dropped"),
}
break;
}
result = lines.next_line() => {
match result {
Ok(Some(line)) => log_emulator_line(&line),
Ok(None) => {
tracing::debug!("Stdout EOF reached");
break;
}
Err(e) => {
tracing::error!("Error reading stdout: {}", e);
return Err(e);
}
}
}
}
}
tracing::info!("Stdout forwarding task exiting");
Ok(())
});
Some(handle)
} else {
None
};
let stderr_task = if use_default_stderr {
let child_stderr = process.stderr.take().expect("stderr should be piped");
let mut shutdown_rx = shutdown_rx.clone();
let handle = tokio::spawn(async move {
tracing::info!("Stderr forwarding task started");
let reader = BufReader::new(child_stderr);
let mut lines = reader.lines();
loop {
tokio::select! {
res = shutdown_rx.wait_for(|v| *v) => {
match res {
Ok(_) => tracing::info!("Stderr forwarding task received shutdown signal"),
Err(_) => tracing::info!("Stderr forwarding task: shutdown sender dropped"),
}
break;
}
result = lines.next_line() => {
match result {
Ok(Some(line)) => log_emulator_line(&line),
Ok(None) => {
tracing::debug!("Stderr EOF reached");
break;
}
Err(e) => {
tracing::error!("Error reading stderr: {}", e);
return Err(e);
}
}
}
}
}
tracing::info!("Stderr forwarding task exiting");
Ok(())
});
Some(handle)
} else {
None
};
let (serial, metadata, discovery_path) = Self::poll_for_emulator(grpc_port).await?;
let owned_process = OwnedProcess {
process,
#[cfg(windows)]
job: Some(job),
#[cfg(unix)]
process_group: Some(process_group),
stdout_task,
stderr_task,
shutdown_tx: Some(shutdown_tx),
};
Ok(Emulator {
owned_process: Some(tokio::sync::Mutex::new(Some(owned_process))),
grpc_port,
serial,
metadata,
discovery_path,
issuer,
})
}
}
pub struct EmulatorClient {
provider: auth::AuthProvider,
interceptor: EmulatorControllerClient<
tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
>,
endpoint: String,
}
impl EmulatorClient {
pub async fn connect_avd(avd: &str) -> Result<Self> {
let emulators = list_emulators().await?;
let matching = emulators
.into_iter()
.find(|e| e.avd_id().map(|id| id == avd).unwrap_or(false));
if let Some(emulator) = matching {
emulator.connect(Some(Duration::from_secs(30)), true).await
} else {
Err(EmulatorError::EmulatorStartFailed(
"No running emulator found".to_string(),
))
}
}
pub async fn connect(endpoint: impl Into<String>) -> Result<Self> {
let endpoint = endpoint.into();
let channel = Channel::from_shared(endpoint.clone())
.map_err(|e| EmulatorError::InvalidUri(e.to_string()))?
.connect()
.await?;
let provider = std::sync::Arc::new(auth::NoOpTokenProvider);
let provider = auth::AuthProvider::new_with_token_provider(provider);
Ok(Self {
interceptor: EmulatorControllerClient::with_interceptor(channel, provider.clone()),
provider,
endpoint,
})
}
pub async fn connect_with_auth(
endpoint: impl Into<String>,
provider: auth::AuthProvider,
) -> Result<Self> {
let endpoint = endpoint.into();
let channel = Channel::from_shared(endpoint.clone())
.map_err(|e| EmulatorError::InvalidUri(e.to_string()))?
.connect()
.await?;
Ok(Self {
interceptor: EmulatorControllerClient::with_interceptor(channel, provider.clone()),
provider,
endpoint,
})
}
pub fn auth_scheme(&self) -> &auth::AuthScheme {
self.provider.auth_scheme()
}
pub fn export_token(&self, auds: &[&str], ttl: Duration) -> Result<auth::BearerToken> {
let token = self.provider.export_token(auds, ttl)?;
Ok(token)
}
pub fn endpoint(&self) -> &str {
&self.endpoint
}
pub fn protocol_mut(
&mut self,
) -> &mut EmulatorControllerClient<
tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
> {
&mut self.interceptor
}
pub fn protocol(
&self,
) -> &EmulatorControllerClient<
tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
> {
&self.interceptor
}
pub async fn wait_until_booted(
&mut self,
timeout: Duration,
poll_interval: Option<Duration>,
) -> Result<Duration> {
let poll_interval = poll_interval.unwrap_or(Duration::from_secs(2));
let start = std::time::Instant::now();
let mut attempt = 0;
loop {
attempt += 1;
let status = self.protocol_mut().get_status(()).await?.into_inner();
if status.booted {
let elapsed = start.elapsed();
tracing::info!(
"Emulator fully booted after {:.1} seconds ({} attempts)",
elapsed.as_secs_f64(),
attempt
);
return Ok(elapsed);
}
tracing::debug!(
"Boot status: {} (attempt {}, elapsed: {:.1}s)",
status.booted,
attempt,
start.elapsed().as_secs_f64()
);
if start.elapsed() >= timeout {
return Err(EmulatorError::ConnectionTimeout);
}
let remaining = timeout.saturating_sub(start.elapsed());
let sleep_duration = poll_interval.min(remaining);
if sleep_duration.is_zero() {
return Err(EmulatorError::ConnectionTimeout);
}
tokio::time::sleep(sleep_duration).await;
}
}
pub async fn shutdown(&mut self, timeout: Option<Duration>) -> Result<()> {
use crate::proto::{VmRunState, vm_run_state::RunState};
let timeout = timeout.unwrap_or(Duration::from_secs(30));
let poll_interval = Duration::from_millis(500);
tracing::info!("Requesting graceful emulator shutdown...");
let shutdown_state = VmRunState {
state: RunState::Shutdown as i32,
};
self.protocol_mut()
.set_vm_state(shutdown_state)
.await
.map_err(|e| {
EmulatorError::EmulatorKillFailed(format!(
"Failed to set VM state to SHUTDOWN: {}",
e
))
})?;
tracing::info!("Shutdown request sent, waiting for VM to shut down...");
let start = std::time::Instant::now();
let mut last_state = None;
loop {
match self.protocol_mut().get_vm_state(()).await {
Ok(response) => {
let vm_state = response.into_inner();
let state = RunState::try_from(vm_state.state).unwrap_or(RunState::Unknown);
if last_state != Some(state) {
tracing::debug!("VM state: {:?}", state);
last_state = Some(state);
}
match state {
RunState::Unknown => {
tracing::info!("VM entered unknown state, proceeding with termination");
break;
}
_ => {
}
}
}
Err(e) => {
tracing::info!(
"Lost connection to emulator ({}), assuming shutdown complete",
e
);
break;
}
}
if start.elapsed() >= timeout {
tracing::warn!(
"Shutdown timeout reached after {:.1} seconds, forcing termination",
timeout.as_secs_f64()
);
break;
}
let remaining = timeout.saturating_sub(start.elapsed());
let sleep_duration = poll_interval.min(remaining);
if sleep_duration.is_zero() {
break;
}
tokio::time::sleep(sleep_duration).await;
}
tracing::info!("VM shutdown complete");
Ok(())
}
}
#[derive(Debug)]
struct OwnedProcess {
process: Child,
#[cfg(windows)]
job: Option<crate::windows::EmulatorJob>,
#[cfg(unix)]
process_group: Option<crate::unix::EmulatorProcessGroup>,
stdout_task: Option<JoinHandle<io::Result<()>>>,
stderr_task: Option<JoinHandle<io::Result<()>>>,
shutdown_tx: Option<tokio::sync::watch::Sender<bool>>,
}
async fn kill_owned_process(mut owned_process: OwnedProcess) -> Result<()> {
let pid = owned_process.process.id();
if let Some(pid) = pid {
tracing::info!("Terminating emulator process with PID {}", pid);
}
#[cfg(windows)]
{
if let Some(job) = &owned_process.job {
if let Err(err) = job.kill() {
tracing::error!("Failed to kill emulator job: {}", err);
return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
}
} else {
if let Err(err) = owned_process.process.start_kill() {
tracing::error!("Failed to kill emulator process: {}", err);
return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
}
}
}
#[cfg(unix)]
{
if let Some(process_group) = &owned_process.process_group {
if let Err(err) = process_group.kill() {
tracing::error!("Failed to kill emulator process group: {}", err);
return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
}
} else {
if let Err(err) = owned_process.process.start_kill() {
tracing::error!("Failed to kill emulator process: {}", err);
return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
}
}
}
#[cfg(not(any(windows, unix)))]
if let Err(err) = owned_process.process.start_kill() {
tracing::error!("Failed to kill emulator process: {}", err);
return Err(EmulatorError::EmulatorKillFailed(err.to_string()));
}
let wait_res = match owned_process.process.wait().await {
Ok(status) => {
if let Some(pid) = pid {
tracing::info!(
"Emulator process with PID {} has exited with status: {:?}",
pid,
status
);
}
Ok(())
}
Err(err) => {
tracing::error!("Failed to wait for emulator process to exit: {}", err);
Err(EmulatorError::EmulatorKillFailed(err.to_string()))
}
};
if let Some(tx) = &owned_process.shutdown_tx {
tracing::info!("Sending shutdown signal to IO forwarding tasks");
let _ = tx.send(true);
}
if let Some(stdout_task) = owned_process.stdout_task.take() {
tracing::info!("Joining stdout forwarding task...");
match stdout_task.await {
Ok(Ok(())) => {
tracing::info!("Stdout forwarding task completed successfully")
}
Ok(Err(e)) => {
tracing::warn!("Stdout forwarding task completed with error: {}", e)
}
Err(e) => {
if e.is_cancelled() {
tracing::debug!("Stdout forwarding task was cancelled");
} else {
tracing::error!("Failed to join stdout forwarding task: {}", e);
}
}
}
}
if let Some(stderr_task) = owned_process.stderr_task.take() {
tracing::info!("Joining stderr forwarding task...");
match stderr_task.await {
Ok(Ok(())) => {
tracing::info!("Stderr forwarding task completed successfully")
}
Ok(Err(e)) => {
tracing::warn!("Stderr forwarding task completed with error: {}", e)
}
Err(e) => {
if e.is_cancelled() {
tracing::debug!("Stderr forwarding task was cancelled");
} else {
tracing::error!("Failed to join stderr forwarding task: {}", e);
}
}
}
}
wait_res
}
#[derive(Debug)]
pub struct Emulator {
owned_process: Option<tokio::sync::Mutex<Option<OwnedProcess>>>,
serial: String,
grpc_port: u16,
discovery_path: PathBuf,
metadata: std::collections::HashMap<String, String>,
issuer: Option<String>,
}
impl Emulator {
pub fn serial(&self) -> &str {
&self.serial
}
pub fn is_owned(&self) -> bool {
self.owned_process.is_some()
}
pub fn discovery_path(&self) -> &Path {
self.discovery_path.as_path()
}
pub fn metadata(&self) -> &std::collections::HashMap<String, String> {
&self.metadata
}
pub fn get_metadata(&self, key: &str) -> Option<&str> {
self.metadata.get(key).map(|s| s.as_str())
}
pub fn requires_jwt_auth(&self) -> bool {
self.get_metadata("grpc.jwk_active").is_some()
}
pub fn avd_name(&self) -> Option<&str> {
self.get_metadata("avd.name")
}
pub fn avd_id(&self) -> Option<&str> {
self.get_metadata("avd.id")
}
pub fn avd_dir(&self) -> Option<&str> {
self.get_metadata("avd.dir")
}
pub fn emulator_version(&self) -> Option<&str> {
self.get_metadata("emulator.version")
}
pub fn emulator_build(&self) -> Option<&str> {
self.get_metadata("emulator.build")
}
pub fn port_serial(&self) -> Option<u16> {
self.get_metadata("port.serial")?.parse().ok()
}
pub fn port_adb(&self) -> Option<u16> {
self.get_metadata("port.adb")?.parse().ok()
}
pub fn cmdline(&self) -> Option<&str> {
self.get_metadata("cmdline")
}
pub fn grpc_endpoint(&self) -> String {
format!("http://localhost:{}", self.grpc_port)
}
pub fn grpc_port(&self) -> u16 {
self.grpc_port
}
pub async fn connect(
&self,
timeout: Option<Duration>,
allow_basic_auth: bool,
) -> Result<EmulatorClient> {
let basic_auth_token = self.get_metadata("grpc.token");
if self.requires_jwt_auth() {
tracing::info!(
"Emulator requires JWT authentication, setting up ES256 token provider..."
);
match self.connect_with_jwt_auth(timeout).await {
Ok(client) => {
tracing::info!("Connected to emulator with JWT authentication.");
return Ok(client);
}
Err(err) => {
tracing::error!("Failed to connect with JWT authentication: {}", err);
if basic_auth_token.is_some() && allow_basic_auth {
tracing::warn!("Falling back to basic authentication...");
} else {
return Err(err);
}
}
}
} else {
tracing::info!("Emulator does not require JWT authentication.");
}
if allow_basic_auth && let Some(token) = basic_auth_token {
tracing::info!("Emulator accepts basic auth, setting up BasicAuthTokenProvider...");
return self.connect_with_basic_auth(token, timeout).await;
}
self.connect_with_noop_auth(timeout).await
}
async fn connect_with_noop_auth(&self, timeout: Option<Duration>) -> Result<EmulatorClient> {
let start = std::time::Instant::now();
let provider = Arc::new(auth::NoOpTokenProvider);
let provider = AuthProvider::new_with_token_provider(provider);
loop {
match EmulatorClient::connect_with_auth(self.grpc_endpoint(), provider.clone()).await {
Ok(mut client) => {
if client.protocol_mut().get_status(()).await.is_ok() {
return Ok(client);
}
}
Err(err) => {
tracing::error!("No-auth connection attempt failed: {}", err);
}
}
if let Some(timeout_duration) = timeout
&& start.elapsed() > timeout_duration
{
return Err(EmulatorError::ConnectionTimeout);
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
async fn connect_with_basic_auth(
&self,
token: &str,
timeout: Option<Duration>,
) -> Result<EmulatorClient> {
let start = std::time::Instant::now();
let provider = Arc::new(auth::BearerTokenProvider::new(token.to_string()));
let provider = AuthProvider::new_with_token_provider(provider);
loop {
match EmulatorClient::connect_with_auth(self.grpc_endpoint(), provider.clone()).await {
Ok(mut client) => {
if client.protocol_mut().get_status(()).await.is_ok() {
return Ok(client);
}
}
Err(err) => {
tracing::error!("Basic auth connection attempt failed: {}", err);
}
}
if let Some(timeout_duration) = timeout
&& start.elapsed() > timeout_duration
{
return Err(EmulatorError::ConnectionTimeout);
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
async fn connect_with_jwt_auth(&self, timeout: Option<Duration>) -> Result<EmulatorClient> {
let jwks_path = self.get_metadata("grpc.jwks").ok_or_else(|| {
EmulatorError::EmulatorStartFailed(
"Emulator requires JWT auth but grpc.jwks path not found in metadata".to_string(),
)
})?;
let jwks_dir = PathBuf::from(jwks_path);
let issuer = self
.issuer
.as_deref()
.unwrap_or("android-studio")
.to_string();
let jwt_provider = tokio::task::spawn_blocking(
move || -> std::result::Result<_, crate::auth::AuthError> {
tracing::info!(
"Generating and registering JWT token provider with issuer '{}'",
issuer
);
let provider = auth::JwtTokenProvider::new_and_register(&jwks_dir, issuer)?;
tracing::info!("JWT token provider registered, waiting for activation...");
provider.wait_for_activation(&jwks_dir, Duration::from_secs(10))?;
let provider = AuthProvider::new_with_token_provider(provider);
Ok(provider)
},
)
.await
.map_err(|err| {
EmulatorError::EmulatorStartFailed(format!(
"Failure running task to register JWT token provider: {err}"
))
})??;
let start = std::time::Instant::now();
loop {
tracing::info!("Attempting JWT connection...");
match EmulatorClient::connect_with_auth(self.grpc_endpoint(), jwt_provider.clone())
.await
{
Ok(mut client) => {
tracing::info!("JWT authentication successful.");
match client.protocol_mut().get_status(()).await {
Ok(_) => {
tracing::info!(
"Successfully connected to emulator with JWT authentication."
);
return Ok(client);
}
Err(err) => {
tracing::error!(
"Failed to get status with JWT authentication: {}",
err
);
}
}
}
Err(err) => {
tracing::error!("JWT connection attempt failed: {}", err);
}
}
if let Some(timeout_duration) = timeout
&& start.elapsed() > timeout_duration
{
return Err(EmulatorError::ConnectionTimeout);
}
tracing::info!("Sleeping before retrying JWT connection...");
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
pub async fn kill(&self) -> Result<()> {
let Some(mutex) = &self.owned_process else {
tracing::warn!("kill() called on an emulator that is not owned by this instance");
return Ok(());
};
let owned = mutex.lock().await.take();
if let Some(owned_process) = owned {
kill_owned_process(owned_process).await?;
tracing::info!("Emulator killed successfully");
Ok(())
} else {
tracing::warn!("kill() called but process was already killed");
Ok(())
}
}
}
impl Drop for Emulator {
fn drop(&mut self) {
if let Some(mutex) = &mut self.owned_process
&& let Some(owned_process) = mutex.get_mut().take()
{
tokio::task::spawn(async move {
if let Err(e) = kill_owned_process(owned_process).await {
tracing::error!("Failed to kill emulator in Drop: {}", e);
}
});
}
}
}
pub async fn get_android_home() -> Result<PathBuf> {
if let Ok(path) = std::env::var("ANDROID_HOME") {
return Ok(PathBuf::from(path));
}
if let Ok(path) = std::env::var("ANDROID_SDK_ROOT") {
return Ok(PathBuf::from(path));
}
#[cfg(target_os = "linux")]
{
if let Some(home) = dirs::home_dir() {
let sdk_path = home.join("Android").join("sdk");
if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
return Ok(sdk_path);
}
let sdk_path = home.join("Android").join("Sdk");
if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
return Ok(sdk_path);
}
}
}
#[cfg(target_os = "macos")]
{
if let Some(home) = dirs::home_dir() {
let sdk_path = home.join("Library").join("Android").join("sdk");
if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
return Ok(sdk_path);
}
}
}
#[cfg(target_os = "windows")]
{
if let Some(local_data) = dirs::data_local_dir() {
let sdk_path = local_data.join("Android").join("Sdk");
if tokio::fs::try_exists(&sdk_path).await.unwrap_or(false) {
return Ok(sdk_path);
}
}
}
Err(EmulatorError::AndroidHomeNotFound)
}
pub async fn list_avds() -> Result<Vec<String>> {
let android_home = get_android_home().await?;
tokio::task::spawn_blocking(move || {
let emulator_path = android_home.join("emulator").join(EMULATOR_BIN);
if !emulator_path.exists() {
return Err(EmulatorError::EmulatorToolNotFound(
emulator_path.display().to_string(),
));
}
let output = std::process::Command::new(&emulator_path)
.arg("-list-avds")
.output()?;
let avds: Vec<String> = String::from_utf8_lossy(&output.stdout)
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if avds.is_empty() {
Err(EmulatorError::NoAvdsFound)
} else {
Ok(avds)
}
})
.await
.map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?
}
fn parse_ini(content: &str) -> std::collections::HashMap<String, String> {
content
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return None;
}
line.split_once('=')
.map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
})
.collect()
}
async fn adb_server() -> Result<adb_client::server::ADBServer> {
use adb_client::server::ADBServer;
let android_home = get_android_home().await?;
let adb_path = android_home.join("platform-tools").join("adb");
let adb_path: String = adb_path
.to_str()
.ok_or_else(|| EmulatorError::AdbError("Invalid Android home path".to_string()))?
.to_string();
let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 5037);
tokio::task::spawn_blocking(move || Ok(ADBServer::new_from_path(addr, Some(adb_path))))
.await
.map_err(|e| EmulatorError::AdbError(format!("Task join error: {}", e)))?
}
pub async fn list_emulators() -> Result<Vec<Emulator>> {
use adb_client::emulator::ADBEmulatorDevice;
let mut server = adb_server().await?;
tokio::task::spawn_blocking(move || {
let mut emulators = vec![];
let devices = server.devices().map_err(|e| {
EmulatorError::IoError(std::io::Error::other(format!(
"Failed to list ADB devices: {}",
e
)))
})?;
for device in devices {
if device.identifier.starts_with("emulator-") {
let mut emulator_device = ADBEmulatorDevice::new(device.identifier.clone(), None)
.map_err(|e| {
EmulatorError::IoError(std::io::Error::other(format!(
"Failed to create ADBEmulatorDevice: {}",
e
)))
})?;
if let Ok(discovery_path) = emulator_device.avd_discovery_path()
&& let Ok(ini_content) = std::fs::read_to_string(&discovery_path)
{
let metadata = parse_ini(&ini_content);
if let Some(port_str) = metadata.get("grpc.port")
&& let Ok(grpc_port) = port_str.parse::<u16>()
{
emulators.push(Emulator {
owned_process: None,
grpc_port,
serial: device.identifier.clone(),
metadata,
discovery_path: discovery_path.clone(),
issuer: None,
});
}
}
}
}
Ok(emulators)
})
.await
.map_err(|e| EmulatorError::EnumerationFailed(format!("Task join error: {}", e)))?
}
pub async fn connect_or_start_emulator(
config: EmulatorConfig,
) -> Result<(EmulatorClient, Option<Emulator>)> {
if let Ok(client) = EmulatorClient::connect_avd(config.avd_id()).await {
tracing::info!("Connected to existing emulator");
return Ok((client, None));
}
tracing::info!("No existing emulator found, starting new one...");
let instance = config.spawn().await?;
tracing::info!("Emulator started at: {}", instance.grpc_endpoint());
tracing::info!("Waiting for emulator to be ready...");
let client = instance
.connect(Some(Duration::from_secs(120)), true)
.await?;
tracing::info!("Connected to new emulator");
Ok((client, Some(instance)))
}