Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): enhance Android dev port forwarding, use host IP for android devices, closes #11137 #11185

Merged
merged 2 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading