Skip to content

Commit a08e6ff

Browse files
authored
feat(cli): enhance Android dev port forwarding, use host IP for android 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
1 parent 6cfe7ed commit a08e6ff

File tree

5 files changed

+297
-184
lines changed

5 files changed

+297
-184
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'tauri-cli': 'patch:enhance'
3+
'@tauri-apps/cli': 'patch:enhance'
4+
---
5+
6+
Enhance port forwarding on `android dev` to be more resilient and tolerate delays when booting up devices.

crates/tauri-cli/src/mobile/android/android_studio_script.rs

Lines changed: 113 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use super::{detect_target_ok, ensure_init, env, get_app, get_config, read_option
66
use crate::{
77
helpers::config::get as get_tauri_config,
88
interface::{AppInterface, Interface},
9+
mobile::CliOptions,
910
Result,
1011
};
1112
use clap::{ArgAction, Parser};
@@ -87,36 +88,17 @@ pub fn command(options: Options) -> Result<()> {
8788
.dev_url
8889
.clone();
8990

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

94-
let devices = adb::device_list(&env).unwrap_or_default();
95-
96-
// clear port forwarding for all devices
97-
for device in &devices {
98-
remove_adb_reverse(&env, device.serial_no(), &forward);
99-
}
100-
101-
// if there's a known target, we should force use it
102-
if let Some(target_device) = &cli_options.target_device {
103-
run_adb_reverse(&env, &target_device.id, &forward, &forward).with_context(|| {
104-
format!(
105-
"failed to forward port with adb, is the {} device connected?",
106-
target_device.name,
107-
)
108-
})?;
109-
} else if devices.len() == 1 {
110-
let device = devices.first().unwrap();
111-
run_adb_reverse(&env, device.serial_no(), &forward, &forward).with_context(|| {
112-
format!(
113-
"failed to forward port with adb, is the {} device connected?",
114-
device.name(),
115-
)
116-
})?;
117-
} else if devices.len() > 1 {
118-
anyhow::bail!("Multiple Android devices are connected ({}), please disconnect devices you do not intend to use so Tauri can determine which to use",
119-
devices.iter().map(|d| d.name()).collect::<Vec<_>>().join(", "));
98+
if localhost {
99+
if let Some(port) = url.port_or_known_default() {
100+
adb_forward_port(port, &env, &cli_options)?;
101+
}
120102
}
121103
}
122104
}
@@ -180,6 +162,102 @@ fn validate_lib(path: &Path) -> Result<()> {
180162
Ok(())
181163
}
182164

165+
fn adb_forward_port(
166+
port: u16,
167+
env: &cargo_mobile2::android::env::Env,
168+
cli_options: &CliOptions,
169+
) -> Result<()> {
170+
let forward = format!("tcp:{port}");
171+
log::info!("Forwarding port {port} with adb");
172+
173+
let mut devices = adb::device_list(env).unwrap_or_default();
174+
// if we could not detect any running device, let's wait a few seconds, it might be booting up
175+
if devices.is_empty() {
176+
log::warn!(
177+
"ADB device list is empty, waiting a few seconds to see if there's any booting device..."
178+
);
179+
180+
let max = 5;
181+
let mut count = 0;
182+
loop {
183+
std::thread::sleep(std::time::Duration::from_secs(1));
184+
185+
devices = adb::device_list(env).unwrap_or_default();
186+
if !devices.is_empty() {
187+
break;
188+
}
189+
190+
count += 1;
191+
if count == max {
192+
break;
193+
}
194+
}
195+
}
196+
197+
let target_device = if let Some(target_device) = &cli_options.target_device {
198+
Some((target_device.id.clone(), target_device.name.clone()))
199+
} else if devices.len() == 1 {
200+
let device = devices.first().unwrap();
201+
Some((device.serial_no().to_string(), device.name().to_string()))
202+
} else if devices.len() > 1 {
203+
anyhow::bail!("Multiple Android devices are connected ({}), please disconnect devices you do not intend to use so Tauri can determine which to use",
204+
devices.iter().map(|d| d.name()).collect::<Vec<_>>().join(", "));
205+
} else {
206+
// when building the app without running to a device, we might have an empty devices list
207+
None
208+
};
209+
210+
if let Some((target_device_serial_no, target_device_name)) = target_device {
211+
let mut already_forwarded = false;
212+
213+
// clear port forwarding for all devices
214+
for device in &devices {
215+
let reverse_list_output = adb_reverse_list(env, device.serial_no())?;
216+
217+
// check if the device has the port forwarded
218+
if String::from_utf8_lossy(&reverse_list_output.stdout).contains(&forward) {
219+
// device matches our target, we can skip forwarding
220+
if device.serial_no() == target_device_serial_no {
221+
log::debug!(
222+
"device {} already has the forward for {}",
223+
device.name(),
224+
forward
225+
);
226+
already_forwarded = true;
227+
}
228+
break;
229+
}
230+
}
231+
232+
// if there's a known target, we should forward the port to it
233+
if already_forwarded {
234+
log::info!("{forward} already forwarded to {target_device_name}");
235+
} else {
236+
loop {
237+
run_adb_reverse(env, &target_device_serial_no, &forward, &forward).with_context(|| {
238+
format!("failed to forward port with adb, is the {target_device_name} device connected?",)
239+
})?;
240+
241+
let reverse_list_output = adb_reverse_list(env, &target_device_serial_no)?;
242+
// wait and retry until the port has actually been forwarded
243+
if String::from_utf8_lossy(&reverse_list_output.stdout).contains(&forward) {
244+
break;
245+
} else {
246+
log::warn!(
247+
"waiting for the port to be forwarded to {}...",
248+
target_device_name
249+
);
250+
std::thread::sleep(std::time::Duration::from_secs(1));
251+
}
252+
}
253+
}
254+
} else {
255+
log::warn!("no running devices detected with ADB; skipping port forwarding");
256+
}
257+
258+
Ok(())
259+
}
260+
183261
fn run_adb_reverse(
184262
env: &cargo_mobile2::android::env::Env,
185263
device_serial_no: &str,
@@ -193,15 +271,13 @@ fn run_adb_reverse(
193271
.run()
194272
}
195273

196-
fn remove_adb_reverse(
274+
fn adb_reverse_list(
197275
env: &cargo_mobile2::android::env::Env,
198276
device_serial_no: &str,
199-
remote: &str,
200-
) {
201-
// ignore errors in case the port is not forwarded
202-
let _ = adb::adb(env, ["-s", device_serial_no, "reverse", "--remove", remote])
277+
) -> std::io::Result<std::process::Output> {
278+
adb::adb(env, ["-s", device_serial_no, "reverse", "--list"])
203279
.stdin_file(os_pipe::dup_stdin().unwrap())
204-
.stdout_file(os_pipe::dup_stdout().unwrap())
205-
.stderr_file(os_pipe::dup_stdout().unwrap())
206-
.run();
280+
.stdout_capture()
281+
.stderr_capture()
282+
.run()
207283
}

crates/tauri-cli/src/mobile/android/dev.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ use crate::{
1414
flock,
1515
},
1616
interface::{AppInterface, Interface, MobileOptions, Options as InterfaceOptions},
17-
mobile::{write_options, CliOptions, DevChild, DevProcess, TargetDevice},
17+
mobile::{
18+
use_network_address_for_dev_url, write_options, CliOptions, DevChild, DevProcess, TargetDevice,
19+
},
1820
ConfigValue, Result,
1921
};
2022
use clap::{ArgAction, Parser};
@@ -31,7 +33,7 @@ use cargo_mobile2::{
3133
target::TargetTrait,
3234
};
3335

34-
use std::env::set_current_dir;
36+
use std::{env::set_current_dir, net::IpAddr};
3537

3638
#[derive(Debug, Clone, Parser)]
3739
#[clap(
@@ -62,6 +64,23 @@ pub struct Options {
6264
pub open: bool,
6365
/// Runs on the given device name
6466
pub device: Option<String>,
67+
/// Force prompting for an IP to use to connect to the dev server on mobile.
68+
#[clap(long)]
69+
pub force_ip_prompt: bool,
70+
/// Use the public network address for the development server.
71+
/// If an actual address it provided, it is used instead of prompting to pick one.
72+
///
73+
/// This option is particularly useful along the `--open` flag when you intend on running on a physical device.
74+
///
75+
/// This replaces the devUrl configuration value to match the public network address host,
76+
/// it is your responsibility to set up your development server to listen on this address
77+
/// by using 0.0.0.0 as host for instance.
78+
///
79+
/// When this is set or when running on an iOS device the CLI sets the `TAURI_DEV_HOST`
80+
/// environment variable so you can check this on your framework's configuration to expose the development server
81+
/// on the public network address.
82+
#[clap(long)]
83+
pub host: Option<Option<IpAddr>>,
6584
/// Disable the built-in dev server for static files.
6685
#[clap(long)]
6786
pub no_dev_server: bool,
@@ -177,6 +196,16 @@ fn run_dev(
177196
metadata: &AndroidMetadata,
178197
noise_level: NoiseLevel,
179198
) -> Result<()> {
199+
// when running on an actual device we must use the network IP
200+
if options.host.is_some()
201+
|| device
202+
.as_ref()
203+
.map(|device| !device.serial_no().starts_with("emulator"))
204+
.unwrap_or(false)
205+
{
206+
use_network_address_for_dev_url(&tauri_config, &mut dev_options, options.force_ip_prompt)?;
207+
}
208+
180209
crate::dev::setup(&interface, &mut dev_options, tauri_config.clone())?;
181210

182211
let interface_options = InterfaceOptions {

0 commit comments

Comments
 (0)