Skip to main content

android_emulator/
lib.rs

1//! Android Emulator gRPC Control Library
2//!
3//! This library provides Rust bindings for controlling Android Emulators via gRPC,
4//! along with utilities for starting and managing emulator instances.
5
6use 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
70/// Find a free gRPC port by querying running emulators via ADB
71///
72/// Returns the first available port starting from 8554, or None if unable to enumerate devices.
73async 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        // Collect all ports currently in use
86        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        // Find first available port starting from 8554
103        (8554..8600).find(|&port| !used_ports.contains(&port))
104    })
105    .await
106    .ok()?
107}
108
109/// Parse and log an emulator output line based on its prefix
110fn 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        // No recognized prefix, log as debug
127        tracing::debug!("{}", line);
128    }
129}
130
131/// gRPC authentication configuration for the emulator
132#[derive(Debug, Clone)]
133pub enum GrpcAuthConfig {
134    /// No authentication required
135    None,
136    /// Basic authentication using console auth token as bearer token
137    Basic,
138    /// JWKS + JWT authentication
139    Jwt {
140        /// Issuer identifier. If None, will be derived from the port as "emulator-{port}"
141        issuer: Option<String>,
142    },
143}
144
145impl Default for GrpcAuthConfig {
146    fn default() -> Self {
147        GrpcAuthConfig::Jwt { issuer: None }
148    }
149}
150
151/// Configuration for starting an Android emulator
152#[derive(Debug)]
153pub struct EmulatorConfig {
154    /// Name of the AVD to start. If None, will use the first available AVD.
155    avd_name: String,
156    /// gRPC port. If None, a free port will be automatically selected.
157    grpc_port: Option<u16>,
158    /// gRPC authentication configuration
159    grpc_auth: GrpcAuthConfig,
160    /// Whether to show the emulator window
161    no_window: bool,
162    /// Whether to load snapshots
163    no_snapshot_load: bool,
164    /// Whether to save snapshots
165    no_snapshot_save: bool,
166    /// Whether to show the boot animation
167    no_boot_anim: bool,
168    /// Whether to disable hardware acceleration (e.g., for running in CI without KVM)
169    no_acceleration: bool,
170    /// Whether to pass -dalvik-vm-checkjni flag to the emulator
171    dalvik_vm_check_jni: bool,
172    /// Allow running multiple instances of the same AVD (without support for snapshots)
173    read_only: bool,
174    /// Optionally quit the emulator after it has booted and been idle for the specified duration (for testing purposes)
175    quit_after_boot: Option<Duration>,
176    /// Additional command-line arguments
177    extra_args: Vec<String>,
178    /// Custom allowlist configuration for JWT authentication
179    /// If None, a default allowlist will be generated
180    grpc_allowlist: Option<auth::GrpcAllowlist>,
181    /// Stdout redirect configuration
182    stdout: Option<Stdio>,
183    /// Stderr redirect configuration
184    stderr: Option<Stdio>,
185}
186
187impl EmulatorConfig {
188    /// Create a new config with a specific AVD name
189    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    /// Get the AVD name that was specified
210    pub fn avd_id(&self) -> &str {
211        &self.avd_name
212    }
213
214    /// Poll ADB until the emulator appears and we can read its metadata
215    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                    // Check if this is our emulator by matching the gRPC port
254                    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    /// Configure gRPC authentication
268    ///
269    /// # Examples
270    ///
271    /// No authentication:
272    /// ```no_run
273    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig};
274    ///
275    /// let config = EmulatorConfig::new("test")
276    ///     .with_grpc_auth(GrpcAuthConfig::None);
277    /// ```
278    ///
279    /// Basic authentication (console token as bearer):
280    /// ```no_run
281    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig};
282    ///
283    /// let config = EmulatorConfig::new("test")
284    ///     .with_grpc_auth(GrpcAuthConfig::Basic)
285    ///     .with_grpc_port(8554);
286    /// ```
287    ///
288    /// JWT mode with custom issuer:
289    /// ```no_run
290    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig};
291    ///
292    /// let config = EmulatorConfig::new("test")
293    ///     .with_grpc_auth(GrpcAuthConfig::Jwt {
294    ///         issuer: Some("mytool".to_string()),
295    ///     })
296    ///     .with_grpc_port(8555);
297    /// ```
298    ///
299    /// JWT mode with auto-derived issuer and auto-selected port:
300    /// ```no_run
301    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig};
302    ///
303    /// let config = EmulatorConfig::new("test")
304    ///     .with_grpc_auth(GrpcAuthConfig::Jwt {
305    ///         issuer: None,  // Will be "emulator-{port}"
306    ///     });
307    /// ```
308    pub fn with_grpc_auth(mut self, auth: GrpcAuthConfig) -> Self {
309        self.grpc_auth = auth;
310        self
311    }
312
313    /// Set the gRPC port
314    ///
315    /// If not specified, a free port will be automatically selected.
316    ///
317    /// # Example
318    ///
319    /// ```no_run
320    /// use android_emulator::EmulatorConfig;
321    ///
322    /// let config = EmulatorConfig::new("test")
323    ///     .with_grpc_port(8554);
324    /// ```
325    pub fn with_grpc_port(mut self, port: u16) -> Self {
326        self.grpc_port = Some(port);
327        self
328    }
329
330    /// Configure whether to show the emulator window
331    ///
332    /// Default is false (headless). Set to true to show the window.
333    pub fn with_window(mut self, show: bool) -> Self {
334        self.no_window = !show;
335        self
336    }
337
338    /// Configure whether to load snapshots
339    ///
340    /// Default is true (load snapshots). Set to false to disable snapshot
341    /// loading on startup.
342    pub fn with_snapshot_load(mut self, load: bool) -> Self {
343        self.no_snapshot_load = !load;
344        self
345    }
346
347    /// Configure whether to save snapshots on exit
348    ///
349    /// Default is true (save snapshots). Set to false to disable snapshot
350    /// saving on exit.
351    pub fn with_snapshot_save(mut self, save: bool) -> Self {
352        self.no_snapshot_save = !save;
353        self
354    }
355
356    /// Configure whether to show the boot animation
357    ///
358    /// Default is true (show boot animation). Set to false to disable the boot
359    /// animation for faster startup.
360    pub fn with_boot_animation(mut self, show: bool) -> Self {
361        self.no_boot_anim = !show;
362        self
363    }
364
365    /// Configure whether to disable hardware acceleration (e.g., for running in
366    /// CI without KVM)
367    ///
368    /// Default is false (use hardware acceleration if available). Set to true
369    /// to disable hardware acceleration
370    pub fn with_acceleration(mut self, enable: bool) -> Self {
371        self.no_acceleration = !enable;
372        self
373    }
374
375    /// Configure whether to pass -dalvik-vm-checkjni flag to the emulator
376    ///
377    /// This can be useful for testing JNI-related functionality and catching
378    /// errors early. Default is false (don't check JNI). Set to true to enable
379    /// JNI checking.
380    pub fn with_dalvik_vm_check_jni(mut self, enable: bool) -> Self {
381        self.dalvik_vm_check_jni = enable;
382        self
383    }
384
385    /// Configure whether to allow running multiple instances of the same AVD
386    /// (without support for snapshots)
387    ///
388    /// Default is false (don't allow multiple instances). Set to true to allow
389    /// multiple instances of the same AVD, but note that snapshots will not
390    /// work in this mode.
391    pub fn with_read_only(mut self, read_only: bool) -> Self {
392        self.read_only = read_only;
393        self
394    }
395
396    /// Configure the emulator to automatically quit after it has booted and
397    /// been idle for the specified duration (for testing purposes)
398    ///
399    /// This can be useful for testing emulator startup and shutdown in CI
400    /// environments.
401    ///
402    /// Default is None (don't quit automatically). Set to Some(duration) to
403    /// enable this behavior.
404    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    /// Set a custom gRPC allowlist configuration for JWT authentication
415    ///
416    /// This allows fine-grained control over which gRPC methods are accessible
417    /// and under what conditions when using JWT authentication. The allowlist
418    /// defines three categories of methods:
419    ///
420    /// - **Unprotected**: Methods that can be invoked without any
421    ///   authentication token
422    /// - **Allowed**: Methods that can be called with a valid JWT token, even
423    ///   without an `aud` claim
424    /// - **Protected**: Methods that require the specific method to be present
425    ///   in the JWT token's `aud` claim
426    ///
427    /// If you don't specify a custom allowlist, a default one will be generated
428    /// using
429    /// [`GrpcAllowlist::default_for_issuer`](auth::GrpcAllowlist::default_for_issuer).
430    ///
431    /// # Arguments
432    ///
433    /// * `allowlist` - A [`GrpcAllowlist`](auth::GrpcAllowlist) configuration
434    ///
435    /// # Example
436    ///
437    /// ```no_run
438    /// use android_emulator::{EmulatorConfig, GrpcAuthConfig, auth::{GrpcAllowlist, AllowlistEntry}};
439    ///
440    /// let allowlist = GrpcAllowlist {
441    ///     unprotected: vec![],  // No methods accessible without auth
442    ///     allowlist: vec![
443    ///         AllowlistEntry {
444    ///             iss: "mytool".to_string(),
445    ///             allowed: vec![
446    ///                 "/android.emulation.control.EmulatorController/.*".to_string(),
447    ///             ],
448    ///             protected: vec![
449    ///                 "/android.emulation.control.SnapshotService/.*".to_string(),
450    ///             ],
451    ///         },
452    ///     ],
453    /// };
454    ///
455    /// let config = EmulatorConfig::new("test")
456    ///     .with_grpc_auth(GrpcAuthConfig::Jwt {
457    ///         issuer: Some("mytool".to_string()),
458    ///     })
459    ///     .with_grpc_allowlist(allowlist);
460    /// ```
461    ///
462    /// # See Also
463    ///
464    /// - [`with_grpc_auth`](Self::with_grpc_auth) - For configuring
465    ///   authentication mode
466    /// - [`GrpcAllowlist::default_for_issuer`](auth::GrpcAllowlist::default_for_issuer)
467    ///   - For the default allowlist
468    pub fn with_grpc_allowlist(mut self, allowlist: auth::GrpcAllowlist) -> Self {
469        self.grpc_allowlist = Some(allowlist);
470        self
471    }
472
473    /// Configure stdout for the emulator process
474    pub fn stdout<T: Into<Stdio>>(mut self, cfg: T) -> Self {
475        self.stdout = Some(cfg.into());
476        self
477    }
478
479    /// Configure stderr for the emulator process
480    pub fn stderr<T: Into<Stdio>>(mut self, cfg: T) -> Self {
481        self.stderr = Some(cfg.into());
482        self
483    }
484
485    /// Start an Android emulator with the given configuration
486    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        // Track whether we're using default piped stdout/stderr for IO forwarding
529        let use_default_stdout = self.stdout.is_none();
530        let use_default_stderr = self.stderr.is_none();
531
532        // Configure stdout (default to piped and run thread to forward to std::io so test capturing works)
533        if let Some(stdout) = self.stdout {
534            cmd.stdout(stdout);
535        } else {
536            cmd.stdout(std::process::Stdio::piped());
537        }
538
539        // Configure stderr (default to piped and run thread to forward to std::io so test capturing works)
540        if let Some(stderr) = self.stderr {
541            cmd.stderr(stderr);
542        } else {
543            cmd.stderr(std::process::Stdio::piped());
544        }
545
546        // Configure gRPC based on authentication mode
547        // All modes start with -grpc <port>
548        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                // No additional flags needed
557                None
558            }
559            GrpcAuthConfig::Basic => {
560                // Add -grpc-use-token for basic authentication
561                cmd.arg("-grpc-use-token");
562                None
563            }
564            GrpcAuthConfig::Jwt { issuer } => {
565                // Derive issuer if not provided
566                let issuer = issuer.unwrap_or_else(|| format!("emulator-{}", grpc_port));
567
568                // Create allowlist file
569                let allowlist = self
570                    .grpc_allowlist
571                    .unwrap_or_else(|| auth::GrpcAllowlist::default_for_issuer(&issuer));
572
573                // Write allowlist to temp file
574                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                // Add -grpc-use-jwt for JWT mode
589                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        // Create IO forwarding thread for stdout if piped (default behavior)
604        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        // Create IO forwarding thread for stderr if piped (default behavior)
621        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        // Poll ADB until the emulator appears and we can read its metadata
638        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
654/// High-level client for controlling an Android Emulator via gRPC
655///
656/// This is a wrapper around the generated `EmulatorControllerClient` that provides
657/// a more convenient API.
658pub 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    /// Try to find an emulator running the specified AVD and connect to it
668    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    /// Connect to an emulator at the specified endpoint without authentication
683    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    /// Connect to an emulator with JWT authentication
701    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    /// Get the authentication scheme used by this client
719    pub fn auth_scheme(&self) -> &auth::AuthScheme {
720        self.provider.auth_scheme()
721    }
722
723    /// Export a bearer token for the given audiences and TTL
724    ///
725    /// This can be used to export a token with specific, limited audience
726    /// claims in order to grant something limited access to the emulator.
727    ///
728    /// Each audience in `auds` will be included as an `aud` claim in the token
729    /// and should correspond to the gRPC method patterns defined in the
730    /// allowlist.
731    ///
732    /// This is only applicable when [`Self::auth_scheme()`] returns
733    /// [`auth::AuthScheme::Jwt`].
734    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    /// Get the endpoint this client is connected to
740    pub fn endpoint(&self) -> &str {
741        &self.endpoint
742    }
743
744    /// Get a mutable reference to the generated gRPC client protobuf binding
745    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    /// Get a reference to the generated gRPC client protobuf binding
754    pub fn protocol(
755        &self,
756    ) -> &EmulatorControllerClient<
757        tonic::service::interceptor::InterceptedService<Channel, auth::AuthProvider>,
758    > {
759        &self.interceptor
760    }
761
762    /// Wait until the emulator has fully booted
763    ///
764    /// This method polls the emulator's boot status until it reports as booted,
765    /// or until the specified timeout is reached.
766    ///
767    /// # Arguments
768    ///
769    /// * `timeout` - Maximum duration to wait for the emulator to boot
770    /// * `poll_interval` - Duration to wait between boot status checks (defaults to 2 seconds if None)
771    ///
772    /// # Returns
773    ///
774    /// Returns `Ok(Duration)` with the time elapsed until boot completed, or an error if the
775    /// timeout is reached or a gRPC error occurs.
776    ///
777    /// # Errors
778    ///
779    /// Returns [`EmulatorError::ConnectionTimeout`] if the emulator doesn't boot within the timeout period.
780    /// Returns [`EmulatorError::GrpcStatus`] if there's a gRPC communication error.
781    ///
782    /// # Example
783    ///
784    /// ```no_run
785    /// use android_emulator::EmulatorClient;
786    /// use std::time::Duration;
787    ///
788    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
789    /// let mut client = EmulatorClient::connect("http://localhost:8554").await?;
790    ///
791    /// // Wait up to 5 minutes for boot, checking every 2 seconds
792    /// let elapsed = client.wait_until_booted(Duration::from_secs(300), None).await?;
793    /// println!("Emulator booted in {:.1} seconds", elapsed.as_secs_f64());
794    /// # Ok(())
795    /// # }
796    /// ```
797    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            // Check if we've exceeded the timeout
828            if start.elapsed() >= timeout {
829                return Err(EmulatorError::ConnectionTimeout);
830            }
831
832            // Calculate remaining time and sleep for the minimum of poll_interval or remaining time
833            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/// Handle to a running emulator instance
846///
847/// This can represent an emulator we spawned with an [`EmulatorConfig`], or an
848/// already-running emulator we discovered with [`list_emulators`].
849#[derive(Debug)]
850pub struct Emulator {
851    is_owned: bool,
852    process: tokio::sync::Mutex<Option<Child>>,
853    serial: String,
854    grpc_port: u16,
855    /// Path to the discovery .ini file
856    discovery_path: PathBuf,
857    /// Runtime metadata from the emulator's discovery .ini file
858    metadata: std::collections::HashMap<String, String>,
859    /// Issuer identifier for JWT authentication (if spawned with custom issuer)
860    issuer: Option<String>,
861    /// IO forwarding thread for stdout (when piped)
862    stdout_thread: tokio::sync::Mutex<Option<JoinHandle<io::Result<()>>>>,
863    /// IO forwarding thread for stderr (when piped)
864    stderr_thread: tokio::sync::Mutex<Option<JoinHandle<io::Result<()>>>>,
865}
866
867impl Emulator {
868    /// Get the serial number of this emulator (if known)
869    pub fn serial(&self) -> &str {
870        &self.serial
871    }
872
873    /// Check if this instance represents an emulator we spawned
874    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    /// Get a reference to all emulator metadata from the runtime .ini file
883    pub fn metadata(&self) -> &std::collections::HashMap<String, String> {
884        &self.metadata
885    }
886
887    /// Get a specific metadata property value
888    pub fn get_metadata(&self, key: &str) -> Option<&str> {
889        self.metadata.get(key).map(|s| s.as_str())
890    }
891
892    /// Check if this emulator requires JWT authentication
893    pub fn requires_jwt_auth(&self) -> bool {
894        self.get_metadata("grpc.jwk_active").is_some()
895    }
896
897    /// Get the AVD name
898    pub fn avd_name(&self) -> Option<&str> {
899        self.get_metadata("avd.name")
900    }
901
902    /// Get the AVD ID
903    pub fn avd_id(&self) -> Option<&str> {
904        self.get_metadata("avd.id")
905    }
906
907    /// Get the AVD directory path
908    pub fn avd_dir(&self) -> Option<&str> {
909        self.get_metadata("avd.dir")
910    }
911
912    /// Get the emulator version
913    pub fn emulator_version(&self) -> Option<&str> {
914        self.get_metadata("emulator.version")
915    }
916
917    /// Get the emulator build ID
918    pub fn emulator_build(&self) -> Option<&str> {
919        self.get_metadata("emulator.build")
920    }
921
922    /// Get the serial port number
923    pub fn port_serial(&self) -> Option<u16> {
924        self.get_metadata("port.serial")?.parse().ok()
925    }
926
927    /// Get the ADB port number
928    pub fn port_adb(&self) -> Option<u16> {
929        self.get_metadata("port.adb")?.parse().ok()
930    }
931
932    /// Get the command line used to launch the emulator
933    pub fn cmdline(&self) -> Option<&str> {
934        self.get_metadata("cmdline")
935    }
936
937    /// Get the gRPC endpoint URL for this emulator
938    pub fn grpc_endpoint(&self) -> String {
939        format!("http://localhost:{}", self.grpc_port)
940    }
941
942    /// Get the gRPC port
943    pub fn grpc_port(&self) -> u16 {
944        self.grpc_port
945    }
946
947    /// Connect to the emulator's gRPC controller
948    ///
949    /// This method will automatically retry the connection if it fails, waiting up to the
950    /// specified timeout duration. At least one connection attempt is always made before
951    /// checking the timeout.
952    ///
953    /// # Arguments
954    ///
955    /// * `timeout` - Maximum time to wait for the connection. If `None`, will wait indefinitely.
956    /// * `allow_basic_auth` - If `true`, will use basic auth (grpc.token / console token) if available.
957    ///
958    /// # Errors
959    ///
960    /// Returns an error if JWT authentication setup fails.
961    pub async fn connect(
962        &self,
963        timeout: Option<Duration>,
964        allow_basic_auth: bool,
965    ) -> Result<EmulatorClient> {
966        // Check if this emulator requires JWT authentication
967        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            // Connect with JWT authentication
975            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        // Check if this emulator accepts basic auth (grpc.token)
994        // This has a lower priority than JWT auth because its mostly only intended for Android Studio use
995        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        // Connect without authentication (use no-op provider)
1001        self.connect_with_noop_auth(timeout).await
1002    }
1003
1004    /// Connect with no-op authentication (internal helper for unauth connections)
1005    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                    // Try a simple call to verify the connection
1014                    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            // Check timeout after the connection attempt
1024            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    /// Connect with basic auth token (internal helper for token-based connections)
1035    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                    // Try a simple call to verify the connection
1049                    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            // Check timeout after the connection attempt
1059            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    /// Connect with ES256 authentication (internal helper for JWT connections)
1070    ///
1071    /// This will verify the JWT connection works, then return an unauthenticated client
1072    /// (since after JWT activation, the emulator will accept unauthenticated connections too)
1073    async fn connect_with_jwt_auth(&self, timeout: Option<Duration>) -> Result<EmulatorClient> {
1074        // Get the JWKS directory from metadata
1075        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        // Generate and register key
1086        let provider = auth::JwtTokenProvider::new_and_register(&jwks_dir, issuer)?;
1087
1088        // Wait for activation (30 second timeout)
1089        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                    // Try a simple call to verify the JWT connection works
1101                    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            // Check timeout after the connection attempt
1122            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    /// Terminate the emulator process and wait for it to fully exit
1133    ///
1134    /// If this instance owns the process (spawned via `spawn()`), this will kill the process
1135    /// and wait for it to exit.
1136    ///
1137    /// If this instance was discovered via `find()`, this returns an error as we don't own
1138    /// the process.
1139    ///
1140    /// To terminate a discovered emulator, use ADB commands directly.
1141    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                // Kill the process
1146                process.kill()?;
1147
1148                // Wait for the process to fully exit
1149                let _ = process.wait()?;
1150
1151                Ok(())
1152            })
1153            .await
1154            .map_err(|e| EmulatorError::EmulatorStartFailed(format!("Task join error: {}", e)))?;
1155        }
1156
1157        // TODO: send a shutdown command via gRPC instead and wait for it to exit
1158        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        // We have exclusive ownership (&mut self), so we can use get_mut() directly
1167        // without needing to lock, which avoids panics in async contexts
1168        if let Some(ref mut process) = *self.process.get_mut() {
1169            let _ = process.kill();
1170        }
1171
1172        // Join IO forwarding threads if they exist
1173        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
1182/// Get the Android SDK home directory
1183///
1184/// This function tries to find the Android SDK in the following order:
1185/// 1. ANDROID_HOME environment variable
1186/// 2. ANDROID_SDK_ROOT environment variable
1187/// 3. Platform-specific default locations:
1188///    - Linux: $HOME/Android/sdk or $HOME/Android/Sdk
1189///    - macOS: $HOME/Library/Android/sdk
1190///    - Windows: %LOCALAPPDATA%/Android/Sdk
1191pub async fn get_android_home() -> Result<PathBuf> {
1192    // Try environment variables first
1193    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    // Try platform-specific default locations
1202    #[cfg(target_os = "linux")]
1203    {
1204        if let Some(home) = dirs::home_dir() {
1205            // Try $HOME/Android/sdk first (lowercase)
1206            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            // Try $HOME/Android/Sdk (capitalized)
1212            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
1242/// Find available Android Virtual Devices (AVDs)
1243pub 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
1273/// Parse a simple INI-style file with key=value pairs
1274fn 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
1304/// Enumerates all running emulators that are discoverable via ADB and
1305/// returns their metadata
1306///
1307/// This is useful for finding emulators that were launched outside of this
1308/// library, such as those launched by Android Studio. It reads the emulator
1309/// metadata from the discovery path .ini files, which include the gRPC port
1310/// and authentication settings.
1311///
1312/// This does not connect to any emulators; use `connect()` on one of the
1313/// returned instance to establish a gRPC connection.
1314pub 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        // Find emulator devices that don't require JWT authentication
1330        for device in devices {
1331            if device.identifier.starts_with("emulator-") {
1332                // Convert to ADBEmulatorDevice to access emulator-specific methods
1333                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                // Get the discovery path for the runtime metadata
1342                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
1372/// Try to connect to an emulator, or start a new one if none is running
1373///
1374/// This function attempts to connect to the first emulator it can find
1375/// that's running the same AVD associated with the provided configuration,
1376/// but if no running emulator is found, it will start a new one with the provided
1377/// configuration.
1378///
1379/// Returns both the connected client and an optional `Emulator` instance
1380/// representing any newly spawned emulator (which will be `None` if we
1381/// connected to an existing emulator).
1382pub async fn connect_or_start_emulator(
1383    config: EmulatorConfig,
1384) -> Result<(EmulatorClient, Option<Emulator>)> {
1385    // Try to connect to existing emulator first
1386    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    // Start a new emulator
1393    let instance = config.spawn().await?;
1394    tracing::info!("Emulator started at: {}", instance.grpc_endpoint());
1395
1396    // Wait for it to be ready and connect
1397    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}