Skip to main content

android/emulator/
endpoint.rs

1//! Resolving an emulator's adb serial to its controller endpoint, and the
2//! warning the driver emits when there is none.
3
4use std::fmt;
5use std::path::PathBuf;
6
7use super::discovery;
8
9/// Where an emulator's controller listens and how to authenticate to it.
10#[derive(Clone, PartialEq, Eq)]
11pub struct Endpoint {
12    /// The adb serial, `emulator-<console port>`.
13    pub serial: String,
14    /// The controller URI, `http://127.0.0.1:<grpc.port>`.
15    pub uri: String,
16    /// Bearer token from the discovery file; present with `-grpc-use-token`.
17    pub token: Option<String>,
18    /// The emulator console's port.
19    pub console_port: Option<u16>,
20    /// The discovery file the endpoint was read from.
21    pub discovery_file: Option<PathBuf>,
22}
23
24impl fmt::Debug for Endpoint {
25    /// Shows whether a token is present, never the token.
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.debug_struct("Endpoint")
28            .field("serial", &self.serial)
29            .field("uri", &self.uri)
30            .field("token", &self.token.as_ref().map(|_| "<redacted>"))
31            .field("console_port", &self.console_port)
32            .field("discovery_file", &self.discovery_file)
33            .finish()
34    }
35}
36
37/// The emulator's control endpoint cannot be used, so taps and frames fall back
38/// to the agent. [`Unavailable::warning`] is the text for the target status.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct Unavailable {
41    pub serial: String,
42    /// Why the endpoint cannot be used.
43    pub reason: String,
44    /// The emulator console's port, when known.
45    pub console_port: Option<u16>,
46}
47
48impl Unavailable {
49    pub fn new(serial: &str, console_port: Option<u16>, reason: impl Into<String>) -> Self {
50        Self {
51            serial: serial.to_owned(),
52            reason: reason.into(),
53            console_port,
54        }
55    }
56
57    /// The gRPC port to suggest: the console port plus 3000, the convention of
58    /// Google's emulator scripts (8554 for emulator-5554), else 8554.
59    pub fn suggested_port(&self) -> u16 {
60        self.console_port
61            .and_then(|console| console.checked_add(3000))
62            .unwrap_or(8554)
63    }
64
65    /// What falls back and how to enable the fast path: the launch flag, or the
66    /// console command that enables it without a restart.
67    pub fn warning(&self) -> String {
68        let port = self.suggested_port();
69        let serial = &self.serial;
70        format!(
71            "emulator control endpoint unavailable for {serial} ({reason}): taps and frames \
72             fall back to the agent. Start the emulator with `-grpc {port} -grpc-use-token` \
73             to use the fast path, or enable it without a restart through the emulator \
74             console command `grpc {port}`, for example `adb -s {serial} emu grpc {port}`.",
75            reason = self.reason
76        )
77    }
78}
79
80impl fmt::Display for Unavailable {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        f.write_str(&self.warning())
83    }
84}
85
86impl std::error::Error for Unavailable {}
87
88/// Resolves `serial` through the discovery files in
89/// [`discovery::candidate_dirs`].
90pub fn resolve(serial: &str) -> Result<Endpoint, Unavailable> {
91    resolve_in(serial, &discovery::candidate_dirs())
92}
93
94/// Resolves `serial` through the discovery files in `dirs`; when several files
95/// name the serial, the newest wins.
96pub fn resolve_in(serial: &str, dirs: &[PathBuf]) -> Result<Endpoint, Unavailable> {
97    let Some(console_port) = discovery::console_port_from_serial(serial) else {
98        return Err(Unavailable::new(
99            serial,
100            None,
101            format!("{serial} is not an emulator serial"),
102        ));
103    };
104    let unavailable = |reason: String| Unavailable::new(serial, Some(console_port), reason);
105    let mut found: Vec<_> = dirs
106        .iter()
107        .filter_map(|dir| discovery::scan_dir(dir).ok())
108        .flatten()
109        .filter_map(|f| match f.parsed {
110            Ok(d) if d.console_port == Some(console_port) => Some((f.modified, f.path, d)),
111            _ => None,
112        })
113        .collect();
114    found.sort_by_key(|(modified, _, _)| *modified);
115    let Some((_, path, discovery)) = found.pop() else {
116        let searched: Vec<String> = dirs.iter().map(|d| d.display().to_string()).collect();
117        return Err(unavailable(format!(
118            "no discovery file names {serial} in {}",
119            searched.join(", ")
120        )));
121    };
122    let Some(uri) = discovery.grpc_endpoint() else {
123        return Err(unavailable(format!(
124            "its discovery file {} has no grpc.port, so it was started without -grpc",
125            path.display()
126        )));
127    };
128    Ok(Endpoint {
129        serial: serial.to_owned(),
130        uri,
131        token: discovery.grpc_token,
132        console_port: Some(console_port),
133        discovery_file: Some(path),
134    })
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use std::fs;
141
142    fn temp_dir(tag: &str) -> PathBuf {
143        let dir = std::env::temp_dir().join(format!(
144            "mobile-agent-endpoint-{tag}-{}",
145            std::process::id()
146        ));
147        let _ = fs::remove_dir_all(&dir);
148        fs::create_dir_all(&dir).unwrap();
149        dir
150    }
151
152    #[test]
153    fn the_warning_names_the_launch_flag_and_the_console_command() {
154        let warning = Unavailable::new("emulator-5554", Some(5554), "connection refused").warning();
155        assert!(warning.contains("emulator-5554"), "{warning}");
156        assert!(warning.contains("connection refused"), "{warning}");
157        assert!(warning.contains("fall back to the agent"), "{warning}");
158        assert!(warning.contains("-grpc 8554 -grpc-use-token"), "{warning}");
159        assert!(warning.contains("`grpc 8554`"), "{warning}");
160        assert!(
161            warning.contains("adb -s emulator-5554 emu grpc 8554"),
162            "{warning}"
163        );
164        assert!(warning.contains("without a restart"), "{warning}");
165    }
166
167    #[test]
168    fn the_suggested_port_follows_the_console_port() {
169        assert_eq!(
170            Unavailable::new("emulator-5556", Some(5556), "").suggested_port(),
171            8556
172        );
173        assert_eq!(
174            Unavailable::new("emulator-5554", None, "").suggested_port(),
175            8554
176        );
177        let warning = Unavailable::new("emulator-5556", Some(5556), "no grpc.port").warning();
178        assert!(warning.contains("-grpc 8556 -grpc-use-token"), "{warning}");
179    }
180
181    #[test]
182    fn a_discovery_file_with_a_grpc_port_resolves_to_an_endpoint() {
183        let dir = temp_dir("resolve");
184        let file = dir.join("pid_42.ini");
185        fs::write(
186            &file,
187            "port.serial=5554\ngrpc.port=8554\ngrpc.token=placeholder+token==\n",
188        )
189        .unwrap();
190        let endpoint = resolve_in("emulator-5554", std::slice::from_ref(&dir));
191        fs::remove_dir_all(&dir).unwrap();
192        let endpoint = endpoint.unwrap();
193        assert_eq!(endpoint.uri, "http://127.0.0.1:8554");
194        assert_eq!(endpoint.token.as_deref(), Some("placeholder+token=="));
195        assert_eq!(endpoint.console_port, Some(5554));
196        assert_eq!(endpoint.discovery_file, Some(file));
197        assert!(!format!("{endpoint:?}").contains("placeholder+token"));
198    }
199
200    #[test]
201    fn no_discovery_file_is_unavailable() {
202        let dir = temp_dir("none");
203        let err = resolve_in("emulator-5556", std::slice::from_ref(&dir)).unwrap_err();
204        fs::remove_dir_all(&dir).unwrap();
205        assert_eq!(err.console_port, Some(5556));
206        assert!(err.reason.contains("no discovery file"), "{}", err.reason);
207        assert!(err.warning().contains("-grpc 8556 -grpc-use-token"));
208    }
209
210    #[test]
211    fn a_discovery_file_without_a_grpc_port_is_unavailable() {
212        let dir = temp_dir("nogrpc");
213        fs::write(dir.join("pid_7.ini"), "port.serial=5556\nport.adb=5557\n").unwrap();
214        let err = resolve_in("emulator-5556", std::slice::from_ref(&dir)).unwrap_err();
215        fs::remove_dir_all(&dir).unwrap();
216        assert!(err.reason.contains("grpc.port"), "{}", err.reason);
217    }
218
219    #[test]
220    fn a_device_serial_has_no_controller() {
221        let err = resolve_in("R58M123ABC", &[]).unwrap_err();
222        assert_eq!(err.console_port, None);
223        assert!(err.reason.contains("not an emulator"), "{}", err.reason);
224    }
225}