feat: device hot-reload, tools-relative adb, auto port bind (#3586)

* wip: bind to 0.0.0.0 instead of 127.0.0.1

* dont hang in background - ignore suspend commands when tty is disabled

* use system adb

* pass configs through to android mobile launch

* fix: line sizes + clear pushes frame

* dont resume window state on mobile

* fix screen sizing for ipad

* attempt and fail to implement codesigning

* Clippy and note about safety in android bindings
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2025-01-20 17:42:13 -08:00
1 parent e199e86dad
commit 8334f7fbaa
22 files changed
+620 -212

No files matched your search

Generated
+43 -4
View File
@@ -3509,6 +3509,7 @@ dependencies = [
"include_dir",
"itertools 0.13.0",
"krates",
"local-ip-address",
"log",
"manganis-core",
"notify",
@@ -3516,6 +3517,7 @@ dependencies = [
"once_cell",
"open",
"path-absolutize",
"plist",
"prettyplease",
"proc-macro2",
"ratatui",
@@ -6298,9 +6300,9 @@ dependencies = [
[[package]]
name = "hstr"
version = "0.2.15"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63d6824358c0fd9a68bb23999ed2ef76c84f79408a26ef7ae53d5f370c94ad36"
checksum = "dae404c0c5d4e95d4858876ab02eecd6a196bb8caa42050dfa809938833fc412"
dependencies = [
"hashbrown 0.14.5",
"new_debug_unreachable",
@@ -7691,6 +7693,18 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "local-ip-address"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3669cf5561f8d27e8fc84cc15e58350e70f557d4d65f70e3154e54cd2f8e1782"
dependencies = [
"libc",
"neli",
"thiserror 1.0.69",
"windows-sys 0.59.0",
]
[[package]]
name = "lock_api"
version = "0.4.12"
@@ -8203,6 +8217,31 @@ dependencies = [
"jni-sys",
]
[[package]]
name = "neli"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1100229e06604150b3becd61a4965d5c70f3be1759544ea7274166f4be41ef43"
dependencies = [
"byteorder",
"libc",
"log",
"neli-proc-macros",
]
[[package]]
name = "neli-proc-macros"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c168194d373b1e134786274020dae7fc5513d565ea2ebb9bc9ff17ffb69106d4"
dependencies = [
"either",
"proc-macro2",
"quote",
"serde",
"syn 1.0.109",
]
[[package]]
name = "nested-suspense"
version = "0.1.0"
@@ -11839,9 +11878,9 @@ dependencies = [
[[package]]
name = "static-self"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "253e76c8c993a7b1b201b0539228b334582153cd4364292822d2c30776d469c7"
checksum = "f6635404b73efc136af3a7956e53c53d4f34b2f16c95a15c438929add0f69412"
dependencies = [
"smallvec",
"static-self-derive",
+2 -7
View File
@@ -99,15 +99,10 @@ macro_rules! read_env_config {
/// For reference, the devserver typically lives on `127.0.0.1:8080` and serves the devserver websocket
/// on `127.0.0.1:8080/_dioxus`.
pub fn devserver_raw_addr() -> Option<SocketAddr> {
// On android, 10.0.2.2 is the default loopback
if cfg!(target_os = "android") {
return Some("10.0.2.2:8080".parse().unwrap());
}
std::env::var(DEVSERVER_RAW_ADDR_ENV)
.map(|s| s.parse().ok())
.unwrap_or_else(|_| "127.0.0.1:8080".to_string())
.parse()
.ok()
.flatten()
}
/// Get the address of the devserver for use over a websocket
+2
View File
@@ -122,7 +122,9 @@ tauri-bundler = { workspace = true }
include_dir = "0.7.4"
flate2 = "1.0.35"
tar = "0.4.43"
local-ip-address = "0.6.3"
dircpy = "0.3.19"
plist = "1.7.0"
[build-dependencies]
built = { version = "=0.7.4", features = ["git2"] }
+50 -18
View File
@@ -3,27 +3,59 @@
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>{{ display_name }}</string>
<key>CFBundleDisplayName</key>
<string>{{ display_name }}</string>
<key>CFBundleExecutable</key>
<string>{{ executable_name }}</string>
<key>CFBundleExecutable</key>
<string>{{ executable_name }}</string>
<key>CFBundleIdentifier</key>
<string>{{ bundle_identifier }}</string>
<key>CFBundleIdentifier</key>
<string>{{ bundle_identifier }}</string>
<key>CFBundleName</key>
<string>{{ bundle_name }}</string>
<key>CFBundleName</key>
<string>{{ bundle_name }}</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en_US</string>
<key>UILaunchStoryboardName</key>
<string></string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en_US</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UISupportsTrueScreenSizeOnMac</key>
<true/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
<string>metal</string>
</array>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
<string>iPadOS</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+1 -1
View File
@@ -156,7 +156,7 @@ impl BuildArgs {
if self.platform == Some(Platform::Android) && self.target_args.arch.is_none() {
tracing::debug!("No android arch provided, attempting to auto detect.");
let arch = Arch::autodetect().await;
let arch = DioxusCrate::autodetect_android_arch().await;
// Some extra logs
let arch = match arch {
+2 -2
View File
@@ -96,8 +96,8 @@ impl ServeArgs {
}
pub(crate) fn is_interactive_tty(&self) -> bool {
use crossterm::tty::IsTty;
std::io::stdout().is_tty() && self.interactive.unwrap_or(true)
use std::io::IsTerminal;
std::io::stdout().is_terminal() && self.interactive.unwrap_or(true)
}
pub(crate) fn should_proxy_build(&self) -> bool {
-44
View File
@@ -1,7 +1,5 @@
use super::*;
use once_cell::sync::OnceCell;
use std::path::Path;
use tokio::process::Command;
/// Information about the target to build
#[derive(Clone, Debug, Default, Deserialize, Parser)]
@@ -73,48 +71,6 @@ pub(crate) enum Arch {
}
impl Arch {
pub(crate) async fn autodetect() -> Option<Self> {
// Try auto detecting arch through adb.
static AUTO_ARCH: OnceCell<Option<Arch>> = OnceCell::new();
match AUTO_ARCH.get() {
Some(a) => *a,
None => {
// TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
let output = Command::new("adb")
.arg("shell")
.arg("uname")
.arg("-m")
.output()
.await;
let out = match output {
Ok(o) => o,
Err(e) => {
tracing::debug!("ADB command failed: {:?}", e);
return None;
}
};
// Parse ADB output
let Ok(out) = String::from_utf8(out.stdout) else {
tracing::debug!("ADB returned unexpected data.");
return None;
};
let trimmed = out.trim().to_string();
tracing::trace!("ADB Returned: `{trimmed:?}`");
// Set the cell
let arch = Arch::try_from(trimmed).ok();
AUTO_ARCH
.set(arch)
.expect("the cell should have been checked empty by the match condition");
arch
}
}
}
pub(crate) fn android_target_triplet(&self) -> &'static str {
match self {
Arch::Arm => "armv7-linux-androideabi",
+4 -22
View File
@@ -4,31 +4,13 @@ use clap::Parser;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
/// The arguments for the address the server will run on
#[derive(Clone, Debug, Parser)]
#[derive(Clone, Debug, Default, Parser)]
pub(crate) struct AddressArguments {
/// The port the server will run on
#[clap(long)]
#[clap(default_value_t = default_port())]
pub(crate) port: u16,
pub(crate) port: Option<u16>,
/// The address the server will run on
#[clap(long, default_value_t = default_address())]
pub(crate) addr: std::net::IpAddr,
}
impl Default for AddressArguments {
fn default() -> Self {
Self {
port: default_port(),
addr: default_address(),
}
}
}
fn default_port() -> u16 {
8080
}
fn default_address() -> IpAddr {
IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))
#[clap(long)]
pub(crate) addr: Option<std::net::IpAddr>,
}
+84 -11
View File
@@ -1,13 +1,15 @@
use crate::CliSettings;
use crate::{config::DioxusConfig, TargetArgs};
use crate::{Arch, CliSettings};
use crate::{Platform, Result};
use anyhow::Context;
use itertools::Itertools;
use krates::{cm::Target, KrateDetails};
use krates::{cm::TargetKind, Cmd, Krates, NodeId};
use once_cell::sync::OnceCell;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::process::Command;
use toml_edit::Item;
// Contains information about the crate we are currently in and the dioxus config for that crate
@@ -616,19 +618,38 @@ impl DioxusCrate {
krates
}
/// Attempt to retrieve the path to ADB
pub(crate) fn android_adb() -> PathBuf {
static PATH: once_cell::sync::Lazy<PathBuf> = once_cell::sync::Lazy::new(|| {
let Some(sdk) = DioxusCrate::android_sdk() else {
return PathBuf::from("adb");
};
let tools = sdk.join("platform-tools");
if tools.join("adb").exists() {
return tools.join("adb");
}
if tools.join("adb.exe").exists() {
return tools.join("adb.exe");
}
PathBuf::from("adb")
});
PATH.clone()
}
pub(crate) fn android_sdk() -> Option<PathBuf> {
var_or_debug("ANDROID_SDK_ROOT")
.or_else(|| var_or_debug("ANDROID_SDK"))
.or_else(|| var_or_debug("ANDROID_HOME"))
}
pub(crate) fn android_ndk(&self) -> Option<PathBuf> {
// "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang"
static PATH: once_cell::sync::Lazy<Option<PathBuf>> = once_cell::sync::Lazy::new(|| {
use std::env::var;
use tracing::debug;
fn var_or_debug(name: &str) -> Option<PathBuf> {
var(name)
.inspect_err(|_| debug!("{name} not set"))
.ok()
.map(PathBuf::from)
}
// attempt to autodetect the ndk path from env vars (usually set by the shell)
let auto_detected_ndk =
var_or_debug("NDK_HOME").or_else(|| var_or_debug("ANDROID_NDK_HOME"));
@@ -655,6 +676,48 @@ impl DioxusCrate {
PATH.clone()
}
pub(crate) async fn autodetect_android_arch() -> Option<Arch> {
// Try auto detecting arch through adb.
static AUTO_ARCH: OnceCell<Option<Arch>> = OnceCell::new();
match AUTO_ARCH.get() {
Some(a) => *a,
None => {
// TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg)
let output = Command::new("adb")
.arg("shell")
.arg("uname")
.arg("-m")
.output()
.await;
let out = match output {
Ok(o) => o,
Err(e) => {
tracing::debug!("ADB command failed: {:?}", e);
return None;
}
};
// Parse ADB output
let Ok(out) = String::from_utf8(out.stdout) else {
tracing::debug!("ADB returned unexpected data.");
return None;
};
let trimmed = out.trim().to_string();
tracing::trace!("ADB Returned: `{trimmed:?}`");
// Set the cell
let arch = Arch::try_from(trimmed).ok();
AUTO_ARCH
.set(arch)
.expect("the cell should have been checked empty by the match condition");
arch
}
}
}
pub(crate) fn mobile_org(&self) -> String {
let identifier = self.bundle_identifier();
let mut split = identifier.splitn(3, '.');
@@ -763,3 +826,13 @@ fn find_main_package(krates: &Krates, package: Option<String>) -> Result<NodeId>
let package = krates.nid_for_kid(kid).unwrap();
Ok(package)
}
fn var_or_debug(name: &str) -> Option<PathBuf> {
use std::env::var;
use tracing::debug;
var(name)
.inspect_err(|_| debug!("{name} not set"))
.ok()
.map(PathBuf::from)
}
+1
View File
@@ -135,6 +135,7 @@ impl TraceController {
let (tui_tx, tui_rx) = unbounded();
TUI_ACTIVE.store(true, Ordering::Relaxed);
TUI_TX.set(tui_tx.clone()).unwrap();
Self { tui_rx }
}
+15 -6
View File
@@ -6,21 +6,30 @@ use tokio::process::Command;
#[derive(Debug, Default)]
pub struct RustcDetails {
pub sysroot: PathBuf,
pub version: String,
}
impl RustcDetails {
/// Find the current sysroot location using the CLI
pub async fn from_cli() -> Result<RustcDetails> {
let output = Command::new("rustc")
let sysroot = Command::new("rustc")
.args(["--print", "sysroot"])
.output()
.await?;
.await
.map(|out| String::from_utf8(out.stdout))?
.context("Failed to extract rustc sysroot output")?;
let stdout =
String::from_utf8(output.stdout).context("Failed to extract rustc sysroot output")?;
let rustc_version = Command::new("rustc")
.args(["--version"])
.output()
.await
.map(|out| String::from_utf8(out.stdout))?
.context("Failed to extract rustc version output")?;
let sysroot = PathBuf::from(stdout.trim());
Ok(Self { sysroot })
Ok(Self {
sysroot: sysroot.trim().into(),
version: rustc_version.trim().into(),
})
}
pub fn has_wasm32_unknown_unknown(&self) -> bool {
+5 -2
View File
@@ -33,14 +33,17 @@ impl AnsiStringLine {
fn trim_end(&mut self) {
for y in 0..self.buf.area.height {
let start_x = self.buf.area.width;
let mut first_non_empty = start_x - 1;
let mut first_non_empty = start_x;
for x in (0..start_x).rev() {
if self.buf.get(x, y) != &buffer::Cell::EMPTY {
break;
}
first_non_empty = x;
}
self.buf.get_mut(first_non_empty, y).set_symbol(SENTINEL);
if first_non_empty != start_x {
self.buf.get_mut(first_non_empty, y).set_symbol(SENTINEL);
}
}
}
+150 -29
View File
@@ -1,4 +1,4 @@
use crate::{AppBundle, Platform, Result};
use crate::{AppBundle, DioxusCrate, Platform, Result};
use anyhow::Context;
use dioxus_cli_opt::process_file_to;
use std::{
@@ -210,7 +210,7 @@ impl AppHandle {
if let Some(bundled_name) = bundled_name.as_ref() {
let target = format!("/data/local/tmp/dx/{}", bundled_name.display());
tracing::debug!("Pushing asset to device: {target}");
let res = tokio::process::Command::new("adb")
let res = tokio::process::Command::new(DioxusCrate::android_adb())
.arg("push")
.arg(&changed_file)
.arg(target)
@@ -323,31 +323,6 @@ impl AppHandle {
/// better support for codesigning and entitlements.
#[allow(unused)]
async fn open_ios_device(&self) -> Result<()> {
// APP_PATH="target/aarch64-apple-ios/debug/bundle/ios/DioxusApp.app"
// # get the device id by jq-ing the json of the device list
// xcrun devicectl list devices --json-output target/deviceid.json
// DEVICE_UUID=$(jq -r '.result.devices[0].identifier' target/deviceid.json)
// xcrun devicectl device install app --device "${DEVICE_UUID}" "${APP_PATH}" --json-output target/xcrun.json
// # get the installation url by jq-ing the json of the device install
// INSTALLATION_URL=$(jq -r '.result.installedApplications[0].installationURL' target/xcrun.json)
// # launch the app
// # todo: we can just background it immediately and then pick it up for loading its logs
// xcrun devicectl device process launch --device "${DEVICE_UUID}" "${INSTALLATION_URL}"
// # # launch the app and put it in background
// # xcrun devicectl device process launch --no-activate --verbose --device "${DEVICE_UUID}" "${INSTALLATION_URL}" --json-output "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}"
// # # Extract background PID of status app
// # STATUS_PID=$(jq -r '.result.process.processIdentifier' "${XCRUN_DEVICE_PROCESS_LAUNCH_LOG_DIR}")
// # "${GIT_ROOT}/scripts/wait-for-metro-port.sh" 2>&1
// # # now that metro is ready, resume the app from background
// # xcrun devicectl device process resume --device "${DEVICE_UUID}" --pid "${STATUS_PID}" > "${XCRUN_DEVICE_PROCESS_RESUME_LOG_DIR}" 2>&1
use serde_json::Value;
let app_path = self.app.build.root_dir();
@@ -403,6 +378,7 @@ impl AppHandle {
}
async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result<String> {
// xcrun devicectl device install app --device <uuid> --path <path> --json-output
let output = Command::new("xcrun")
.args([
"devicectl",
@@ -489,6 +465,151 @@ impl AppHandle {
unimplemented!("dioxus-cli doesn't support ios devices yet.")
}
#[allow(unused)]
async fn codesign_ios(&self) -> Result<()> {
const CODESIGN_ERROR: &str = r#"This is likely because you haven't
- Created a provisioning profile before
- Accepted the Apple Developer Program License Agreement
The agreement changes frequently and might need to be accepted again.
To accept the agreement, go to https://developer.apple.com/account
To create a provisioning profile, follow the instructions here:
https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certificates"#;
let profiles_folder = dirs::home_dir()
.context("Your machine has no home-dir")?
.join("Library/MobileDevice/Provisioning Profiles");
if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() {
tracing::error!(
r#"No provisioning profiles found when trying to codesign the app.
We checked the folder: {}
{CODESIGN_ERROR}
"#,
profiles_folder.display()
)
}
let identities = Command::new("security")
.args(["find-identity", "-v", "-p", "codesigning"])
.output()
.await
.context("Failed to run `security find-identity -v -p codesigning`")
.map(|e| {
String::from_utf8(e.stdout)
.context("Failed to parse `security find-identity -v -p codesigning`")
})??;
// Parsing this:
// 51ADE4986E0033A5DB1C794E0D1473D74FD6F871 "Apple Development: jkelleyrtp@gmail.com (XYZYZY)"
let app_dev_name = regex::Regex::new(r#""Apple Development: (.+)""#)
.unwrap()
.captures(&identities)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str())
.context(
"Failed to find Apple Development in `security find-identity -v -p codesigning`",
)?;
// Acquire the provision file
let provision_file = profiles_folder
.read_dir()?
.flatten()
.find(|entry| {
entry
.file_name()
.to_str()
.map(|s| s.contains("mobileprovision"))
.unwrap_or_default()
})
.context("Failed to find a provisioning profile. \n\n{CODESIGN_ERROR}")?;
// The .mobileprovision file has some random binary thrown into into, but it's still basically a plist
// Let's use the plist markers to find the start and end of the plist
fn cut_plist(bytes: &[u8], byte_match: &[u8]) -> Option<usize> {
bytes
.windows(byte_match.len())
.enumerate()
.rev()
.find(|(_, slice)| *slice == byte_match)
.map(|(i, _)| i + byte_match.len())
}
let bytes = std::fs::read(provision_file.path())?;
let cut1 = cut_plist(&bytes, b"<plist").context("Failed to parse .mobileprovision file")?;
let cut2 = cut_plist(&bytes, r#"</dict>"#.as_bytes())
.context("Failed to parse .mobileprovision file")?;
let sub_bytes = &bytes[(cut1 - 6)..cut2];
let mbfile: ProvisioningProfile =
plist::from_bytes(sub_bytes).context("Failed to parse .mobileprovision file")?;
#[derive(serde::Deserialize, Debug)]
struct ProvisioningProfile {
#[serde(rename = "TeamIdentifier")]
team_identifier: Vec<String>,
#[serde(rename = "ApplicationIdentifierPrefix")]
application_identifier_prefix: Vec<String>,
#[serde(rename = "Entitlements")]
entitlements: Entitlements,
}
#[derive(serde::Deserialize, Debug)]
struct Entitlements {
#[serde(rename = "application-identifier")]
application_identifier: String,
#[serde(rename = "keychain-access-groups")]
keychain_access_groups: Vec<String>,
}
let entielements_xml = format!(
r#"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>application-identifier</key>
<string>{APPLICATION_IDENTIFIER}</string>
<key>keychain-access-groups</key>
<array>
<string>{APP_ID_ACCESS_GROUP}.*</string>
</array>
<key>get-task-allow</key>
<true/>
<key>com.apple.developer.team-identifier</key>
<string>{TEAM_IDENTIFIER}</string>
</dict></plist>
"#,
APPLICATION_IDENTIFIER = mbfile.entitlements.application_identifier,
APP_ID_ACCESS_GROUP = mbfile.entitlements.keychain_access_groups[0],
TEAM_IDENTIFIER = mbfile.team_identifier[0],
);
// write to a temp file
let temp_file = tempfile::NamedTempFile::new()?;
std::fs::write(temp_file.path(), entielements_xml)?;
// codesign the app
let output = Command::new("codesign")
.args([
"--force",
"--entitlements",
temp_file.path().to_str().unwrap(),
"--sign",
app_dev_name,
])
.arg(self.app.build.root_dir())
.output()
.await
.context("Failed to codesign the app")?;
if !output.status.success() {
let stderr = String::from_utf8(output.stderr).unwrap_or_default();
return Err(format!("Failed to codesign the app: {stderr}").into());
}
Ok(())
}
async fn open_android_sim(&self, envs: Vec<(&'static str, String)>) {
let apk_path = self.app.apk_path();
let full_mobile_app_name = self.app.build.krate.full_mobile_app_name();
@@ -497,7 +618,7 @@ impl AppHandle {
tokio::task::spawn(async move {
// Install
// adb install -r app-debug.apk
if let Err(e) = Command::new("adb")
if let Err(e) = Command::new(DioxusCrate::android_adb())
.arg("install")
.arg("-r")
.arg(apk_path)
@@ -513,7 +634,7 @@ impl AppHandle {
// adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity
let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,);
if let Err(e) = Command::new("adb")
if let Err(e) = Command::new(DioxusCrate::android_adb())
.arg("shell")
.arg("am")
.arg("start")
+2 -2
View File
@@ -45,11 +45,11 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
let krate = args.load_krate().await?;
// Note that starting the builder will queue up a build immediately
let mut screen = Output::start(&args).await?;
let mut builder = Builder::start(&krate, args.build_args())?;
let mut devserver = WebServer::start(&krate, &args)?;
let mut watcher = Watcher::start(&krate, &args);
let mut runner = AppRunner::start(&krate);
let mut screen = Output::start(&args)?;
// This is our default splash screen. We might want to make this a fancier splash screen in the future
// Also, these commands might not be the most important, but it's all we've got enabled right now
@@ -58,7 +58,7 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
Serving your Dioxus app: {} 🚀
• Press `ctrl+c` to exit the server
• Press `r` to rebuild the app
• Press `o` to open the app
• Press `p` to toggle automatic rebuilds
• Press `v` to toggle verbose logging
• Press `/` for more commands and shortcuts
Learn more at https://dioxuslabs.com/learn/0.6/getting_started
+127 -28
View File
@@ -1,6 +1,7 @@
use crate::{
serve::{ansi_buffer::AnsiStringLine, Builder, ServeUpdate, Watcher, WebServer},
BuildStage, BuildUpdate, DioxusCrate, Platform, ServeArgs, TraceContent, TraceMsg, TraceSrc,
BuildStage, BuildUpdate, DioxusCrate, Platform, RustcDetails, ServeArgs, TraceContent,
TraceMsg, TraceSrc,
};
use crossterm::{
cursor::{Hide, Show},
@@ -28,7 +29,7 @@ use tracing::Level;
const TICK_RATE_MS: u64 = 100;
const VIEWPORT_MAX_WIDTH: u16 = 100;
const VIEWPORT_HEIGHT_SMALL: u16 = 5;
const VIEWPORT_HEIGHT_BIG: u16 = 12;
const VIEWPORT_HEIGHT_BIG: u16 = 13;
/// The TUI that drives the console output.
///
@@ -63,6 +64,8 @@ pub struct Output {
// ! needs to be wrapped in an &mut since `render stateful widget` requires &mut... but our
// "render" method only borrows &self (for no particular reason at all...)
throbber: RefCell<throbber_widgets_tui::ThrobberState>,
rustc_details: RustcDetails,
}
#[allow(unused)]
@@ -76,17 +79,9 @@ struct RenderState<'a> {
}
impl Output {
pub(crate) fn start(cfg: &ServeArgs) -> io::Result<Self> {
pub(crate) async fn start(cfg: &ServeArgs) -> crate::Result<Self> {
let mut output = Self {
term: Rc::new(RefCell::new(
Terminal::with_options(
CrosstermBackend::new(stdout()),
TerminalOptions {
viewport: Viewport::Inline(VIEWPORT_HEIGHT_SMALL),
},
)
.ok(),
)),
term: Rc::new(RefCell::new(None)),
interactive: cfg.is_interactive_tty(),
dx_version: format!(
"{}-{}",
@@ -95,7 +90,6 @@ impl Output {
),
platform: cfg.build_arguments.platform.expect("To be resolved by now"),
events: None,
// messages: Vec::new(),
more_modal_open: false,
pending_logs: VecDeque::new(),
throbber: RefCell::new(throbber_widgets_tui::ThrobberState::default()),
@@ -107,6 +101,7 @@ impl Output {
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval
},
rustc_details: RustcDetails::from_cli().await?,
};
output.startup()?;
@@ -127,11 +122,24 @@ impl Output {
original_hook(info);
}));
enable_raw_mode()?;
stdout()
.execute(Hide)?
.execute(EnableFocusChange)?
.execute(EnableBracketedPaste)?;
// Check if writing the terminal is going to block infinitely.
// If it does, we should disable interactive mode. This ensures we work with programs like `bg`
// which suspend the process and cause us to block when writing output.
if Self::enable_raw_mode().is_err() {
self.term.take();
self.interactive = false;
return Ok(());
}
self.term.replace(
Terminal::with_options(
CrosstermBackend::new(stdout()),
TerminalOptions {
viewport: Viewport::Inline(VIEWPORT_HEIGHT_SMALL),
},
)
.ok(),
);
// Initialize the event stream here - this is optional because an EvenStream in a non-interactive
// terminal will cause a panic instead of simply doing nothing.
@@ -142,6 +150,36 @@ impl Output {
Ok(())
}
/// Enable raw mode, but don't let it block forever.
///
/// This lets us check if writing to tty is going to block forever and then recover, allowing
/// interopability with programs like `bg`.
fn enable_raw_mode() -> io::Result<()> {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
// Ignore SIGTSTP, SIGTTIN, and SIGTTOU
_ = signal(SignalKind::from_raw(20))?; // SIGTSTP
_ = signal(SignalKind::from_raw(21))?; // SIGTTIN
_ = signal(SignalKind::from_raw(22))?; // SIGTTOU
}
use std::io::IsTerminal;
if !stdout().is_terminal() {
return io::Result::Err(io::Error::new(io::ErrorKind::Other, "Not a terminal"));
}
enable_raw_mode()?;
stdout()
.execute(Hide)?
.execute(EnableFocusChange)?
.execute(EnableBracketedPaste)?;
Ok(())
}
/// Call the shutdown functions that might mess with the terminal settings - see the related code
/// in "startup" for more details about what we need to unset
pub(crate) fn shutdown(&self) -> io::Result<()> {
@@ -163,6 +201,10 @@ impl Output {
use futures_util::future::OptionFuture;
use futures_util::StreamExt;
if !self.interactive {
return std::future::pending().await;
}
// Wait for the next user event or animation tick
loop {
let next = OptionFuture::from(self.events.as_mut().map(|f| f.next()));
@@ -226,7 +268,16 @@ impl Output {
stdout()
.execute(Clear(ClearType::All))?
.execute(Clear(ClearType::Purge))?;
_ = self.term.borrow_mut().as_mut().map(|t| t.clear());
// Clear the terminal and push the frame to the bottom
_ = self.term.borrow_mut().as_mut().map(|t| {
let frame_rect = t.get_frame().area();
let term_size = t.size().unwrap();
let remaining_space = term_size
.height
.saturating_sub(frame_rect.y + frame_rect.height);
t.insert_before(remaining_space, |_| {})
});
}
// Toggle the more modal by swapping the the terminal with a new one
@@ -611,7 +662,7 @@ impl Output {
self.render_feature_list(frame, app_features, state);
// todo(jon) should we write https ?
let address = match state.server.server_address() {
let address = match state.server.displayed_address() {
Some(address) => format!("http://{}", address).blue(),
None => "no server address".dark_gray(),
};
@@ -656,16 +707,20 @@ impl Output {
);
}
fn render_more_modal(&self, frame: &mut Frame<'_>, area: Rect, _state: RenderState) {
fn render_more_modal(&self, frame: &mut Frame<'_>, area: Rect, state: RenderState) {
let [col1, col2] =
Layout::horizontal([Constraint::Length(50), Constraint::Fill(1)]).areas(area);
let [top, bottom] = Layout::vertical([Constraint::Fill(1), Constraint::Length(2)])
.horizontal_margin(1)
.areas(area);
.areas(col1);
let meta_list: [_; 5] = Layout::vertical([
let meta_list: [_; 6] = Layout::vertical([
Constraint::Length(1), // spacing
Constraint::Length(1), // item 1
Constraint::Length(1), // item 2
Constraint::Length(1), // item 3
Constraint::Length(1), // item 4
Constraint::Length(1), // Spacing
])
.areas(top);
@@ -680,15 +735,27 @@ impl Output {
frame.render_widget(
Paragraph::new(Line::from(vec![
"rustc: ".gray(),
"1.79.9 (nightly)".yellow(),
self.rustc_details.version.as_str().yellow(),
])),
meta_list[2],
);
frame.render_widget(
Paragraph::new(Line::from(vec!["Hotreload: ".gray(), "rsx only".yellow()])),
Paragraph::new(Line::from(vec![
"Hotreload: ".gray(),
"rsx and assets".yellow(),
])),
meta_list[3],
);
let server_address = match state.server.server_address() {
Some(address) => format!("http://{}", address).yellow(),
None => "no address".dark_gray(),
};
frame.render_widget(
Paragraph::new(Line::from(vec!["Network: ".gray(), server_address])),
meta_list[4],
);
let links_list: [_; 2] =
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(bottom);
@@ -707,6 +774,35 @@ impl Output {
])),
links_list[1],
);
let cmds = [
"",
"r: rebuild the app",
"o: open the app",
"p: pause rebuilds",
"v: toggle verbose logs",
"t: toggle tracing logs ",
"c: clear the screen",
"/: toggle more commands",
];
let layout: [_; 8] = Layout::vertical(cmds.iter().map(|_| Constraint::Length(1)))
.horizontal_margin(1)
.areas(col2);
for (idx, cmd) in cmds.iter().enumerate() {
if cmd.is_empty() {
continue;
}
let (cmd, detail) = cmd.split_once(": ").unwrap_or((cmd, ""));
frame.render_widget(
Paragraph::new(Line::from(vec![
cmd.gray(),
": ".gray(),
detail.dark_gray(),
])),
layout[idx],
);
}
}
/// Render borders around the terminal, forcing an inner clear while we're at it
@@ -905,9 +1001,12 @@ impl Output {
line = line.dark_gray();
}
let line_length: usize = line.spans.iter().map(|f| f.content.len()).sum();
lines.push(AnsiStringLine::new(line_length.max(100) as _).render(&line));
// Create the ansi -> raw string line with a width of either the viewport width or the max width
let line_length = line.styled_graphemes(Style::default()).count();
lines.push(
AnsiStringLine::new(line_length.max(VIEWPORT_MAX_WIDTH.into()) as _)
.render(&line),
);
}
}
+9 -1
View File
@@ -194,12 +194,15 @@ impl AppRunner {
/// Open an existing app bundle, if it exists
pub(crate) async fn open_existing(&mut self, devserver: &WebServer) -> Result<()> {
let fullstack_address = devserver.proxied_server_address();
if let Some((_, app)) = self
.running
.iter_mut()
.find(|(platform, _)| **platform != Platform::Server)
{
app.open(devserver.devserver_address(), None, true).await?;
app.open(devserver.devserver_address(), fullstack_address, true)
.await?;
}
Ok(())
}
@@ -228,6 +231,11 @@ impl AppRunner {
continue;
}
// Special-case the Cargo.toml file - we want updates here to cause a full rebuild
if path.file_name().and_then(|v| v.to_str()) == Some("Cargo.toml") {
return None;
}
// Otherwise, it might be an asset and we should look for it in all the running apps
for runner in self.running.values() {
if let Some(bundled_name) = runner.hotreload_bundled_asset(&path).await {
+61 -21
View File
@@ -31,10 +31,9 @@ use serde::{Deserialize, Serialize};
use std::{
convert::Infallible,
fs, io,
net::{IpAddr, SocketAddr, TcpListener},
net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
path::Path,
sync::Arc,
sync::RwLock,
sync::{Arc, RwLock},
};
use tower_http::{
cors::Any,
@@ -49,7 +48,8 @@ use tower_http::{
/// which carries all the message types. This would make it easier for us to add more message types
/// and better tooling on the pages that we serve.
pub(crate) struct WebServer {
devserver_ip: IpAddr,
devserver_exposed_ip: IpAddr,
devserver_bind_ip: IpAddr,
devserver_port: u16,
proxied_port: Option<u16>,
hot_reload_sockets: Vec<WebSocket>,
@@ -71,18 +71,43 @@ impl WebServer {
let (hot_reload_sockets_tx, hot_reload_sockets_rx) = futures_channel::mpsc::unbounded();
let (build_status_sockets_tx, build_status_sockets_rx) = futures_channel::mpsc::unbounded();
let devserver_ip = args.address.addr;
let devserver_port = args.address.port;
let devserver_address = SocketAddr::new(devserver_ip, devserver_port);
const SELF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0));
// Use 0.0.0.0 as the default address if none is specified - this will let us expose the
// devserver to the network (for other devices like phones/embedded)
let devserver_bind_ip = args.address.addr.unwrap_or(SELF_IP);
// If the user specified a port, use that, otherwise use any available port, preferring 8080
let devserver_port = args
.address
.port
.unwrap_or_else(|| get_available_port(devserver_bind_ip, Some(8080)).unwrap_or(8080));
// All servers will end up behind us (the devserver) but on a different port
// This is so we can serve a loading screen as well as devtools without anything particularly fancy
let proxied_port = args
.should_proxy_build()
.then(|| get_available_port(devserver_ip))
.then(|| get_available_port(devserver_bind_ip, None))
.flatten();
let proxied_address = proxied_port.map(|port| SocketAddr::new(devserver_ip, port));
// Create the listener that we'll pass into the devserver, but save its IP here so
// we can display it to the user in the tui
let devserver_bind_address = SocketAddr::new(devserver_bind_ip, devserver_port);
let listener = std::net::TcpListener::bind(devserver_bind_address).with_context(|| {
anyhow::anyhow!(
"Failed to bind server to: {devserver_bind_address}, is there another devserver running?\nTo run multiple devservers, use the --port flag to specify a different port"
)
})?;
// If the IP is 0.0.0.0, we need to get the actual IP of the machine
// This will let ios/android/network clients connect to the devserver
let devserver_exposed_ip = if devserver_bind_ip == SELF_IP {
local_ip_address::local_ip().unwrap_or(devserver_bind_ip)
} else {
devserver_bind_ip
};
let proxied_address = proxied_port.map(|port| SocketAddr::new(devserver_exposed_ip, port));
// Set up the router with some shared state that we'll update later to reflect the current state of the build
let build_status = SharedStatus::new_with_starting_build();
@@ -95,14 +120,6 @@ impl WebServer {
build_status.clone(),
)?;
// Create the listener that we'll pass into the devserver, but save its IP here so
// we can display it to the user in the tui
let listener = std::net::TcpListener::bind(devserver_address).with_context(|| {
anyhow::anyhow!(
"Failed to bind server to: {devserver_address}, is there another devserver running?\nTo run multiple devservers, use the --port flag to specify a different port"
)
})?;
// And finally, start the server mainloop
tokio::spawn(devserver_mainloop(
krate.config.web.https.clone(),
@@ -113,7 +130,8 @@ impl WebServer {
Ok(Self {
build_status,
proxied_port,
devserver_ip,
devserver_bind_ip,
devserver_exposed_ip,
devserver_port,
hot_reload_sockets: Default::default(),
build_status_sockets: Default::default(),
@@ -315,13 +333,13 @@ impl WebServer {
/// Get the address the devserver should run on
pub fn devserver_address(&self) -> SocketAddr {
SocketAddr::new(self.devserver_ip, self.devserver_port)
SocketAddr::new(self.devserver_exposed_ip, self.devserver_port)
}
// Get the address the server should run on if we're serving the user's server
pub fn proxied_server_address(&self) -> Option<SocketAddr> {
self.proxied_port
.map(|port| SocketAddr::new(self.devserver_ip, port))
.map(|port| SocketAddr::new(self.devserver_exposed_ip, port))
}
pub fn server_address(&self) -> Option<SocketAddr> {
@@ -330,6 +348,19 @@ impl WebServer {
_ => self.proxied_server_address(),
}
}
/// Get the address the server is running - showing 127.0.0.1 if the devserver is bound to 0.0.0.0
/// This is designed this way to not confuse users who expect the devserver to be bound to localhost
/// ... which it is, but they don't know that 0.0.0.0 also serves localhost.
pub fn displayed_address(&self) -> Option<SocketAddr> {
let mut address = self.server_address()?;
if self.devserver_bind_ip == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) {
address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), address.port());
}
Some(address)
}
}
async fn devserver_mainloop(
@@ -382,6 +413,7 @@ fn build_devserver_router(
if args.should_proxy_build() {
// For fullstack, liveview, and server, forward all requests to the inner server
let address = fullstack_address.unwrap();
tracing::debug!("Proxying requests to fullstack server at {address}");
router = router.nest_service("/",super::proxy::proxy_to(
format!("http://{address}").parse().unwrap(),
true,
@@ -616,7 +648,15 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> {
///
/// Todo: we might want to do this on every new build in case the OS tries to bind things to this port
/// and we don't already have something bound to it. There's no great way of "reserving" a port.
fn get_available_port(address: IpAddr) -> Option<u16> {
fn get_available_port(address: IpAddr, prefer: Option<u16>) -> Option<u16> {
// First, try to bind to the preferred port
if let Some(port) = prefer {
if let Ok(_listener) = TcpListener::bind((address, port)) {
return Some(port);
}
}
// Otherwise, try to bind to any port and return the first one we can
TcpListener::bind((address, 0))
.map(|listener| listener.local_addr().unwrap().port())
.ok()
+2 -2
View File
@@ -327,7 +327,7 @@ impl WasmBindgen {
}
async fn verify_local_install(&self) -> anyhow::Result<()> {
tracing::info!(
tracing::trace!(
"Verifying wasm-bindgen-cli@{} is installed in the path",
self.version
);
@@ -355,7 +355,7 @@ impl WasmBindgen {
}
async fn verify_managed_install(&self) -> anyhow::Result<()> {
tracing::info!(
tracing::trace!(
"Verifying wasm-bindgen-cli@{} is installed in the tool directory",
self.version
);
+4
View File
@@ -485,6 +485,10 @@ impl App {
// Write this to the target dir so we can pick back up
#[cfg(debug_assertions)]
fn resume_from_state(&mut self, webview: &WebviewInstance) {
if cfg!(target_os = "android") || cfg!(target_os = "ios") {
return;
}
if let Ok(state) = std::fs::read_to_string(restore_file()) {
if let Ok(state) = serde_json::from_str::<PreservedWindowState>(&state) {
let window = &webview.desktop_context.window;
+1 -1
View File
@@ -288,7 +288,7 @@ pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
}
}
use std::{io::Read, ptr::NonNull};
use std::ptr::NonNull;
let ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
+1 -1
View File
@@ -87,7 +87,7 @@ impl LaunchBuilder {
#[cfg_attr(docsrs, doc(cfg(feature = "mobile")))]
pub fn mobile() -> LaunchBuilder {
LaunchBuilder {
launch_fn: |root, contexts, cfg| dioxus_mobile::launch::launch(root, contexts, cfg),
launch_fn: |root, contexts, cfg| dioxus_mobile::launch_cfg(root, contexts, cfg),
contexts: Vec::new(),
configs: Vec::new(),
}
+54 -10
View File
@@ -4,18 +4,18 @@
pub use dioxus_desktop::*;
use dioxus_lib::prelude::*;
use std::any::Any;
use std::sync::Mutex;
pub mod launch_bindings {
use std::any::Any;
use super::*;
pub fn launch(
root: fn() -> Element,
_contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
_platform_config: Vec<Box<dyn Any>>,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) {
super::launch(root);
super::launch_cfg(root, contexts, platform_config);
}
pub fn launch_virtual_dom(_virtual_dom: VirtualDom, _desktop_config: Config) -> ! {
@@ -24,27 +24,71 @@ pub mod launch_bindings {
}
/// Launch via the binding API
pub fn launch(incoming: fn() -> Element) {
pub fn launch(root: fn() -> Element) {
launch_cfg(root, vec![], vec![]);
}
pub fn launch_cfg(
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
) {
#[cfg(target_os = "android")]
{
*APP_FN_PTR.lock().unwrap() = Some(incoming);
*APP_OBJECTS.lock().unwrap() = Some(BoundLaunchObjects {
root,
contexts,
platform_config,
});
}
#[cfg(not(target_os = "android"))]
{
dioxus_desktop::launch::launch(incoming, vec![], Default::default());
dioxus_desktop::launch::launch(root, contexts, platform_config);
}
}
static APP_FN_PTR: Mutex<Option<fn() -> Element>> = Mutex::new(None);
/// We need to store the root function and contexts in a static so that when the tao bindings call
/// "start_app", that the original function arguments are still around.
///
/// If you look closely, you'll notice that we impl Send for this struct. This would normally be
/// unsound. However, we know that the thread that created these objects ("main()" - see JNI_OnLoad)
/// is finished once `start_app` is called. This is similar to how an Rc<T> is technically safe
/// to move between threads if you can prove that no other thread is using the Rc<T> at the same time.
/// Crates like https://crates.io/crates/sendable exist that build on this idea but with runtimk,
/// validation that the current thread is the one that created the object.
///
/// Since `main()` completes, the only reader of this data will be `start_app`, so it's okay to
/// impl this as Send/Sync.
///
/// Todo(jon): the visibility of functions in this module is too public. Make sure to hide them before
/// releasing 0.7.
struct BoundLaunchObjects {
root: fn() -> Element,
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
platform_config: Vec<Box<dyn Any>>,
}
unsafe impl Send for BoundLaunchObjects {}
unsafe impl Sync for BoundLaunchObjects {}
static APP_OBJECTS: Mutex<Option<BoundLaunchObjects>> = Mutex::new(None);
#[doc(hidden)]
pub fn root() {
let app = APP_FN_PTR
let app = APP_OBJECTS
.lock()
.expect("APP_FN_PTR lock failed")
.take()
.expect("Android to have set the app trampoline");
dioxus_desktop::launch::launch(app, vec![], Default::default());
let BoundLaunchObjects {
root,
contexts,
platform_config,
} = app;
dioxus_desktop::launch::launch(root, contexts, platform_config);
}
/// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.