use std::fmt;
use std::path::PathBuf;
use super::discovery;
#[derive(Clone, PartialEq, Eq)]
pub struct Endpoint {
pub serial: String,
pub uri: String,
pub token: Option<String>,
pub console_port: Option<u16>,
pub discovery_file: Option<PathBuf>,
}
impl fmt::Debug for Endpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Endpoint")
.field("serial", &self.serial)
.field("uri", &self.uri)
.field("token", &self.token.as_ref().map(|_| "<redacted>"))
.field("console_port", &self.console_port)
.field("discovery_file", &self.discovery_file)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Unavailable {
pub serial: String,
pub reason: String,
pub console_port: Option<u16>,
}
impl Unavailable {
pub fn new(serial: &str, console_port: Option<u16>, reason: impl Into<String>) -> Self {
Self {
serial: serial.to_owned(),
reason: reason.into(),
console_port,
}
}
pub fn suggested_port(&self) -> u16 {
self.console_port
.and_then(|console| console.checked_add(3000))
.unwrap_or(8554)
}
pub fn warning(&self) -> String {
let port = self.suggested_port();
let serial = &self.serial;
format!(
"emulator control endpoint unavailable for {serial} ({reason}): taps and frames \
fall back to the agent. Start the emulator with `-grpc {port} -grpc-use-token` \
to use the fast path, or enable it without a restart through the emulator \
console command `grpc {port}`, for example `adb -s {serial} emu grpc {port}`.",
reason = self.reason
)
}
}
impl fmt::Display for Unavailable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.warning())
}
}
impl std::error::Error for Unavailable {}
pub fn resolve(serial: &str) -> Result<Endpoint, Unavailable> {
resolve_in(serial, &discovery::candidate_dirs())
}
pub fn resolve_in(serial: &str, dirs: &[PathBuf]) -> Result<Endpoint, Unavailable> {
let Some(console_port) = discovery::console_port_from_serial(serial) else {
return Err(Unavailable::new(
serial,
None,
format!("{serial} is not an emulator serial"),
));
};
let unavailable = |reason: String| Unavailable::new(serial, Some(console_port), reason);
let mut found: Vec<_> = dirs
.iter()
.filter_map(|dir| discovery::scan_dir(dir).ok())
.flatten()
.filter_map(|f| match f.parsed {
Ok(d) if d.console_port == Some(console_port) => Some((f.modified, f.path, d)),
_ => None,
})
.collect();
found.sort_by_key(|(modified, _, _)| *modified);
let Some((_, path, discovery)) = found.pop() else {
let searched: Vec<String> = dirs.iter().map(|d| d.display().to_string()).collect();
return Err(unavailable(format!(
"no discovery file names {serial} in {}",
searched.join(", ")
)));
};
let Some(uri) = discovery.grpc_endpoint() else {
return Err(unavailable(format!(
"its discovery file {} has no grpc.port, so it was started without -grpc",
path.display()
)));
};
Ok(Endpoint {
serial: serial.to_owned(),
uri,
token: discovery.grpc_token,
console_port: Some(console_port),
discovery_file: Some(path),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"mobile-agent-endpoint-{tag}-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn the_warning_names_the_launch_flag_and_the_console_command() {
let warning = Unavailable::new("emulator-5554", Some(5554), "connection refused").warning();
assert!(warning.contains("emulator-5554"), "{warning}");
assert!(warning.contains("connection refused"), "{warning}");
assert!(warning.contains("fall back to the agent"), "{warning}");
assert!(warning.contains("-grpc 8554 -grpc-use-token"), "{warning}");
assert!(warning.contains("`grpc 8554`"), "{warning}");
assert!(
warning.contains("adb -s emulator-5554 emu grpc 8554"),
"{warning}"
);
assert!(warning.contains("without a restart"), "{warning}");
}
#[test]
fn the_suggested_port_follows_the_console_port() {
assert_eq!(
Unavailable::new("emulator-5556", Some(5556), "").suggested_port(),
8556
);
assert_eq!(
Unavailable::new("emulator-5554", None, "").suggested_port(),
8554
);
let warning = Unavailable::new("emulator-5556", Some(5556), "no grpc.port").warning();
assert!(warning.contains("-grpc 8556 -grpc-use-token"), "{warning}");
}
#[test]
fn a_discovery_file_with_a_grpc_port_resolves_to_an_endpoint() {
let dir = temp_dir("resolve");
let file = dir.join("pid_42.ini");
fs::write(
&file,
"port.serial=5554\ngrpc.port=8554\ngrpc.token=placeholder+token==\n",
)
.unwrap();
let endpoint = resolve_in("emulator-5554", std::slice::from_ref(&dir));
fs::remove_dir_all(&dir).unwrap();
let endpoint = endpoint.unwrap();
assert_eq!(endpoint.uri, "http://127.0.0.1:8554");
assert_eq!(endpoint.token.as_deref(), Some("placeholder+token=="));
assert_eq!(endpoint.console_port, Some(5554));
assert_eq!(endpoint.discovery_file, Some(file));
assert!(!format!("{endpoint:?}").contains("placeholder+token"));
}
#[test]
fn no_discovery_file_is_unavailable() {
let dir = temp_dir("none");
let err = resolve_in("emulator-5556", std::slice::from_ref(&dir)).unwrap_err();
fs::remove_dir_all(&dir).unwrap();
assert_eq!(err.console_port, Some(5556));
assert!(err.reason.contains("no discovery file"), "{}", err.reason);
assert!(err.warning().contains("-grpc 8556 -grpc-use-token"));
}
#[test]
fn a_discovery_file_without_a_grpc_port_is_unavailable() {
let dir = temp_dir("nogrpc");
fs::write(dir.join("pid_7.ini"), "port.serial=5556\nport.adb=5557\n").unwrap();
let err = resolve_in("emulator-5556", std::slice::from_ref(&dir)).unwrap_err();
fs::remove_dir_all(&dir).unwrap();
assert!(err.reason.contains("grpc.port"), "{}", err.reason);
}
#[test]
fn a_device_serial_has_no_controller() {
let err = resolve_in("R58M123ABC", &[]).unwrap_err();
assert_eq!(err.console_port, None);
assert!(err.reason.contains("not an emulator"), "{}", err.reason);
}
}