feat(cli): restart Android emulator if it is disconnected from adb

needs https://github.com/tauri-apps/cargo-mobile2/pull/495 and https://github.com/tauri-apps/cargo-mobile2/pull/493
This commit is contained in:
Lucas Nogueira
2025-10-16 09:55:10 -03:00
parent 3397fd9bfe
commit 0786768c4e
5 changed files with 219 additions and 35 deletions

View File

@@ -0,0 +1,6 @@
---
"@tauri-apps/cli": minor:feat
"tauri-cli": minor:feat
---
Prompt to restart the Android emulator if it is not connected to adb.

57
Cargo.lock generated
View File

@@ -234,6 +234,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-log",
"tauri-plugin-process",
"tauri-plugin-sample",
"tiny_http",
]
@@ -1055,9 +1056,9 @@ dependencies = [
[[package]]
name = "cargo-mobile2"
version = "0.21.1"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcea7efeaac9f0fd9f886f43a13dde186a1e2266fe6b53a42659e4e0689570de"
checksum = "bab92dd12ad373b27af98de101f2ec7524634617c354c9ff34b880b3da8e5de3"
dependencies = [
"colored",
"core-foundation 0.10.0",
@@ -2078,9 +2079,9 @@ checksum = "7454e41ff9012c00d53cf7f475c5e3afa3b91b7c90568495495e8d9bf47a1055"
[[package]]
name = "duct"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6ce170a0e8454fa0f9b0e5ca38a6ba17ed76a50916839d217eb5357e05cdfde"
checksum = "d7478638a31d1f1f3d6c9f5e57c76b906a04ac4879d6fd0fb6245bc88f73fd0b"
dependencies = [
"libc",
"os_pipe",
@@ -4280,9 +4281,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.169"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libfuzzer-sys"
@@ -5394,12 +5395,12 @@ dependencies = [
[[package]]
name = "os_pipe"
version = "1.2.1"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -7816,19 +7817,20 @@ dependencies = [
[[package]]
name = "shared_child"
version = "1.0.1"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c"
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
dependencies = [
"libc",
"windows-sys 0.59.0",
"sigchld",
"windows-sys 0.60.2",
]
[[package]]
name = "shared_thread"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7a6f98357c6bb0ebace19b22220e5543801d9de90ffe77f8abb27c056bac064"
checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0"
[[package]]
name = "shell-words"
@@ -7843,10 +7845,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.17"
name = "sigchld"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
dependencies = [
"libc",
"os_pipe",
"signal-hook",
]
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
@@ -8840,6 +8853,16 @@ dependencies = [
"time",
]
[[package]]
name = "tauri-plugin-process"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-sample"
version = "0.1.0"

View File

@@ -36,7 +36,7 @@ name = "cargo-tauri"
path = "src/main.rs"
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies]
cargo-mobile2 = { version = "0.21.1", default-features = false }
cargo-mobile2 = { version = "0.22.1", default-features = false }
[dependencies]
jsonrpsee = { version = "0.24", features = ["server"] }

View File

@@ -13,7 +13,7 @@ use crate::{
use clap::{ArgAction, Parser};
use cargo_mobile2::{
android::{adb, target::Target},
android::{adb, device::ConnectionStatus, target::Target},
opts::Profile,
target::{call_for_targets_with_fallback, TargetTrait},
};
@@ -214,7 +214,11 @@ fn adb_forward_port(
let forward = format!("tcp:{port}");
log::info!("Forwarding port {port} with adb");
let mut devices = adb::device_list(env).unwrap_or_default();
let mut devices = adb::device_list(env)
.unwrap_or_default()
.into_iter()
.filter(|d| d.status() == ConnectionStatus::Connected)
.collect::<Vec<_>>();
// if we could not detect any running device, let's wait a few seconds, it might be booting up
if devices.is_empty() {
log::warn!(
@@ -226,7 +230,11 @@ fn adb_forward_port(
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
devices = adb::device_list(env).unwrap_or_default();
devices = adb::device_list(env)
.unwrap_or_default()
.into_iter()
.filter(|d| d.status() == ConnectionStatus::Connected)
.collect::<Vec<_>>();
if !devices.is_empty() {
break;
}

View File

@@ -6,7 +6,7 @@ use cargo_mobile2::{
android::{
adb,
config::{Config as AndroidConfig, Metadata as AndroidMetadata, Raw as RawAndroidConfig},
device::Device,
device::{ConnectionStatus, Device},
emulator,
env::Env,
target::Target,
@@ -444,7 +444,11 @@ fn delete_codegen_vars() {
}
fn adb_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
let device_list = adb::device_list(env).context("failed to detect connected Android devices")?;
let device_list = adb::device_list(env)
.context("failed to detect connected Android devices")?
.into_iter()
.filter(|d| d.status() == ConnectionStatus::Connected)
.collect::<Vec<_>>();
if !device_list.is_empty() {
let device = if let Some(t) = target {
let (device, score) = device_list
@@ -530,31 +534,174 @@ fn emulator_prompt(env: &'_ Env, target: Option<&str>) -> Result<emulator::Emula
}
}
enum EmulatorStatus {
Offline { serial_no: String },
Connected,
}
fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
if let Ok(device) = adb_device_prompt(env, target) {
Ok(device)
} else {
let emulator = emulator_prompt(env, target)?;
log::info!("Starting emulator {}", emulator.name());
emulator
.start_detached(env)
.context("failed to start emulator")?;
let emulator_status = match adb::device_list(env) {
Ok(devices) => {
// emulator might be running but disconnected from adb
devices
.iter()
.find(|d| d.name() == emulator.name())
.and_then(|d| match d.status() {
ConnectionStatus::Offline | ConnectionStatus::Unauthorized => {
Some(EmulatorStatus::Offline {
serial_no: d.serial_no().to_string(),
})
}
ConnectionStatus::Connected => Some(EmulatorStatus::Connected),
_ => None,
})
}
// failed to get device information, check if the device name matches the emulator name
Err(
adb::device_list::Error::ModelFailed {
serial_no,
error: adb::get_prop::Error::CommandFailed { command: _, error },
}
| adb::device_list::Error::AbiFailed {
serial_no,
error: adb::get_prop::Error::CommandFailed { command: _, error },
},
) => {
if error.kind() == std::io::ErrorKind::TimedOut {
// if the device name matches the emulator name, the emulator is already running and marked as connected
// but we cannot connect to it
adb::device_name(env, &serial_no).map_or(None, |device_name| {
if device_name == emulator.name() {
Some(EmulatorStatus::Offline { serial_no })
} else {
None
}
})
} else {
None
}
}
Err(_) => None,
};
let emulator_already_running = emulator_status.is_some();
match emulator_status {
Some(EmulatorStatus::Offline { serial_no }) => {
// emulator is available but not connected to adb, we must restart it
log::info!("Emulator is not connected, we need to restart it");
restart_emulator(&env, &serial_no, &emulator)?;
}
Some(EmulatorStatus::Connected) => {
// emulator is already connected to adb
// this is technically unreachable because we queried the device list with adb_device_prompt
}
None => {
log::info!("Starting emulator {}", emulator.name());
emulator
.start_detached(env)
.context("failed to start emulator")?;
}
}
let mut tries = 0;
loop {
sleep(Duration::from_secs(2));
if let Ok(device) = adb_device_prompt(env, Some(emulator.name())) {
return Ok(device);
// we do not filter for connected devices to detect emulators that are not connected to our adb anymore
match adb::device_list(env) {
Ok(devices) => {
if let Some(device) = devices.into_iter().find(|d| d.name() == emulator.name()) {
if device.status() == ConnectionStatus::Connected {
return Ok(device);
}
}
if tries >= 3 {
log::info!("Waiting for emulator to start... (maybe the emulator is unauthorized or offline, run `adb devices` to check)");
} else {
log::info!("Waiting for emulator to start...");
}
tries += 1;
}
Err(
adb::device_list::Error::ModelFailed {
serial_no,
error: adb::get_prop::Error::CommandFailed { command: _, error },
}
| adb::device_list::Error::AbiFailed {
serial_no,
error: adb::get_prop::Error::CommandFailed { command: _, error },
},
) => {
if emulator_already_running && error.kind() == std::io::ErrorKind::TimedOut {
log::info!("Emulator is not responding, we need to restart it");
restart_emulator(&env, &serial_no, &emulator)?;
tries = 0;
} else {
log::error!("failed to get properties for device {serial_no}: {error}");
}
}
Err(e) => {
log::error!("failed to list devices with adb: {e}");
tries += 1;
}
}
if tries >= 3 {
log::info!("Waiting for emulator to start... (maybe the emulator is unauthorized or offline, run `adb devices` to check)");
} else {
log::info!("Waiting for emulator to start...");
}
tries += 1;
}
}
}
fn restart_emulator(env: &Env, serial_no: &str, emulator: &emulator::Emulator) -> Result<()> {
let granted_permission_to_restart =
crate::helpers::prompts::confirm("Do you want to restart the emulator?", Some(true))
.unwrap_or_default();
if !granted_permission_to_restart {
crate::error::bail!(
"Cannot connect to the emulator, please restart it manually (a full boot might be required)"
);
}
adb::adb(env, &["-s", &serial_no, "emu", "kill"])
.run()
.context("failed to reboot emulator")?;
log::info!("Waiting for emulator to exit...");
loop {
let devices = adb::device_list(env).unwrap_or_default();
if devices
.into_iter()
.find(|d| d.serial_no() == serial_no)
.is_none()
{
break;
}
sleep(Duration::from_secs(1));
}
log::info!("Restarting emulator with full boot");
let mut tries = 0;
loop {
// wait a bit to make sure we can restart the emulator
sleep(Duration::from_secs(2));
match emulator.start_detached_with_options(env, emulator::StartOptions::new().full_boot()) {
Ok(_) => break,
Err(e) => {
tries += 1;
if tries >= 3 {
return Err(e).context("failed to start emulator");
} else {
log::error!("failed to start emulator, retrying...");
}
}
}
}
Ok(())
}
fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> {
device_prompt(env, None).map(|device| device.target()).ok()
}