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:
1 parent
e199e86dad
commit
8334f7fbaa
22 files changed
+620
-212
No files matched your search
Generated
+43
-4
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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>,
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user