Skip to content

Commit

Permalink
feat(cli): enhance Android dev port forwarding, use host IP for andro…
Browse files Browse the repository at this point in the history
…id devices, closes #11137 (#11185)

* feat(cli): enhance Android dev port forwarding, closes #11137

this changes the `android dev` port forwarding (that is actually handled by the `android-studio-script` command - triggered by our Gradle plugin) with some enhancements:

- make the whole process more resilient by checking if the port was actually forwarded and rerunning the `adb reverse` command until it tells us the forward is ready
- if the `adb devices` list is empty, retry a few times (waiting a few seconds) to tolerate devices being booted - slows down "raw builds" (Build Project Android Studio menu for instance) that shouldn't happen often anyway - if you're running `android dev` you're usually running the app on a device instead of simply testing builds

* use host IP to run on android physical device
  • Loading branch information
lucasfernog authored Oct 2, 2024
1 parent 6cfe7ed commit a08e6ff
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 184 deletions.
6 changes: 6 additions & 0 deletions .changes/enhance-android-port-forwarding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'tauri-cli': 'patch:enhance'
'@tauri-apps/cli': 'patch:enhance'
---

Enhance port forwarding on `android dev` to be more resilient and tolerate delays when booting up devices.
150 changes: 113 additions & 37 deletions crates/tauri-cli/src/mobile/android/android_studio_script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use super::{detect_target_ok, ensure_init, env, get_app, get_config, read_option
use crate::{
helpers::config::get as get_tauri_config,
interface::{AppInterface, Interface},
mobile::CliOptions,
Result,
};
use clap::{ArgAction, Parser};
Expand Down Expand Up @@ -87,36 +88,17 @@ pub fn command(options: Options) -> Result<()> {
.dev_url
.clone();

if let Some(port) = dev_url.and_then(|url| url.port_or_known_default()) {
let forward = format!("tcp:{port}");
log::info!("Forwarding port {port} with adb");
if let Some(url) = dev_url {
let localhost = match url.host() {
Some(url::Host::Domain(d)) => d == "localhost",
Some(url::Host::Ipv4(i)) => i == std::net::Ipv4Addr::LOCALHOST,
_ => false,
};

let devices = adb::device_list(&env).unwrap_or_default();

// clear port forwarding for all devices
for device in &devices {
remove_adb_reverse(&env, device.serial_no(), &forward);
}

// if there's a known target, we should force use it
if let Some(target_device) = &cli_options.target_device {
run_adb_reverse(&env, &target_device.id, &forward, &forward).with_context(|| {
format!(
"failed to forward port with adb, is the {} device connected?",
target_device.name,
)
})?;
} else if devices.len() == 1 {
let device = devices.first().unwrap();
run_adb_reverse(&env, device.serial_no(), &forward, &forward).with_context(|| {
format!(
"failed to forward port with adb, is the {} device connected?",
device.name(),
)
})?;
} else if devices.len() > 1 {
anyhow::bail!("Multiple Android devices are connected ({}), please disconnect devices you do not intend to use so Tauri can determine which to use",
devices.iter().map(|d| d.name()).collect::<Vec<_>>().join(", "));
if localhost {
if let Some(port) = url.port_or_known_default() {
adb_forward_port(port, &env, &cli_options)?;
}
}
}
}
Expand Down Expand Up @@ -180,6 +162,102 @@ fn validate_lib(path: &Path) -> Result<()> {
Ok(())
}

fn adb_forward_port(
port: u16,
env: &cargo_mobile2::android::env::Env,
cli_options: &CliOptions,
) -> Result<()> {
let forward = format!("tcp:{port}");
log::info!("Forwarding port {port} with adb");

let mut devices = adb::device_list(env).unwrap_or_default();
// 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!(
"ADB device list is empty, waiting a few seconds to see if there's any booting device..."
);

let max = 5;
let mut count = 0;
loop {
std::thread::sleep(std::time::Duration::from_secs(1));

devices = adb::device_list(env).unwrap_or_default();
if !devices.is_empty() {
break;
}

count += 1;
if count == max {
break;
}
}
}

let target_device = if let Some(target_device) = &cli_options.target_device {
Some((target_device.id.clone(), target_device.name.clone()))
} else if devices.len() == 1 {
let device = devices.first().unwrap();
Some((device.serial_no().to_string(), device.name().to_string()))
} else if devices.len() > 1 {
anyhow::bail!("Multiple Android devices are connected ({}), please disconnect devices you do not intend to use so Tauri can determine which to use",
devices.iter().map(|d| d.name()).collect::<Vec<_>>().join(", "));
} else {
// when building the app without running to a device, we might have an empty devices list
None
};

if let Some((target_device_serial_no, target_device_name)) = target_device {
let mut already_forwarded = false;

// clear port forwarding for all devices
for device in &devices {
let reverse_list_output = adb_reverse_list(env, device.serial_no())?;

// check if the device has the port forwarded
if String::from_utf8_lossy(&reverse_list_output.stdout).contains(&forward) {
// device matches our target, we can skip forwarding
if device.serial_no() == target_device_serial_no {
log::debug!(
"device {} already has the forward for {}",
device.name(),
forward
);
already_forwarded = true;
}
break;
}
}

// if there's a known target, we should forward the port to it
if already_forwarded {
log::info!("{forward} already forwarded to {target_device_name}");
} else {
loop {
run_adb_reverse(env, &target_device_serial_no, &forward, &forward).with_context(|| {
format!("failed to forward port with adb, is the {target_device_name} device connected?",)
})?;

let reverse_list_output = adb_reverse_list(env, &target_device_serial_no)?;
// wait and retry until the port has actually been forwarded
if String::from_utf8_lossy(&reverse_list_output.stdout).contains(&forward) {
break;
} else {
log::warn!(
"waiting for the port to be forwarded to {}...",
target_device_name
);
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
}
} else {
log::warn!("no running devices detected with ADB; skipping port forwarding");
}

Ok(())
}

fn run_adb_reverse(
env: &cargo_mobile2::android::env::Env,
device_serial_no: &str,
Expand All @@ -193,15 +271,13 @@ fn run_adb_reverse(
.run()
}

fn remove_adb_reverse(
fn adb_reverse_list(
env: &cargo_mobile2::android::env::Env,
device_serial_no: &str,
remote: &str,
) {
// ignore errors in case the port is not forwarded
let _ = adb::adb(env, ["-s", device_serial_no, "reverse", "--remove", remote])
) -> std::io::Result<std::process::Output> {
adb::adb(env, ["-s", device_serial_no, "reverse", "--list"])
.stdin_file(os_pipe::dup_stdin().unwrap())
.stdout_file(os_pipe::dup_stdout().unwrap())
.stderr_file(os_pipe::dup_stdout().unwrap())
.run();
.stdout_capture()
.stderr_capture()
.run()
}
33 changes: 31 additions & 2 deletions crates/tauri-cli/src/mobile/android/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use crate::{
flock,
},
interface::{AppInterface, Interface, MobileOptions, Options as InterfaceOptions},
mobile::{write_options, CliOptions, DevChild, DevProcess, TargetDevice},
mobile::{
use_network_address_for_dev_url, write_options, CliOptions, DevChild, DevProcess, TargetDevice,
},
ConfigValue, Result,
};
use clap::{ArgAction, Parser};
Expand All @@ -31,7 +33,7 @@ use cargo_mobile2::{
target::TargetTrait,
};

use std::env::set_current_dir;
use std::{env::set_current_dir, net::IpAddr};

#[derive(Debug, Clone, Parser)]
#[clap(
Expand Down Expand Up @@ -62,6 +64,23 @@ pub struct Options {
pub open: bool,
/// Runs on the given device name
pub device: Option<String>,
/// Force prompting for an IP to use to connect to the dev server on mobile.
#[clap(long)]
pub force_ip_prompt: bool,
/// Use the public network address for the development server.
/// If an actual address it provided, it is used instead of prompting to pick one.
///
/// This option is particularly useful along the `--open` flag when you intend on running on a physical device.
///
/// This replaces the devUrl configuration value to match the public network address host,
/// it is your responsibility to set up your development server to listen on this address
/// by using 0.0.0.0 as host for instance.
///
/// When this is set or when running on an iOS device the CLI sets the `TAURI_DEV_HOST`
/// environment variable so you can check this on your framework's configuration to expose the development server
/// on the public network address.
#[clap(long)]
pub host: Option<Option<IpAddr>>,
/// Disable the built-in dev server for static files.
#[clap(long)]
pub no_dev_server: bool,
Expand Down Expand Up @@ -177,6 +196,16 @@ fn run_dev(
metadata: &AndroidMetadata,
noise_level: NoiseLevel,
) -> Result<()> {
// when running on an actual device we must use the network IP
if options.host.is_some()
|| device
.as_ref()
.map(|device| !device.serial_no().starts_with("emulator"))
.unwrap_or(false)
{
use_network_address_for_dev_url(&tauri_config, &mut dev_options, options.force_ip_prompt)?;
}

crate::dev::setup(&interface, &mut dev_options, tauri_config.clone())?;

let interface_options = InterfaceOptions {
Expand Down
Loading

0 comments on commit a08e6ff

Please sign in to comment.