feat: android bundling, red/blue exe names, session cache (#3608)
* feat: android bundling * fix: add hashing to exe names for red/blue exes * feat: implement session cache for window restoration
This commit is contained in:
1 parent
024285c548
commit
d342630a0a
11 files changed
+334
-185
No files matched your search
@@ -5,17 +5,16 @@ use crate::{
|
||||
use dioxus_core::internal::TemplateGlobalKey;
|
||||
use dioxus_devtools_types::HotReloadMsg;
|
||||
use dioxus_html::HtmlCtx;
|
||||
use futures_util::{future::OptionFuture, stream::FuturesUnordered, FutureExt};
|
||||
use futures_util::future::OptionFuture;
|
||||
use ignore::gitignore::Gitignore;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
net::SocketAddr,
|
||||
path::PathBuf,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
pub(crate) struct AppRunner {
|
||||
pub(crate) running: HashMap<Platform, AppHandle>,
|
||||
pub(crate) running: Option<AppHandle>,
|
||||
pub(crate) krate: DioxusCrate,
|
||||
pub(crate) file_map: HotreloadFilemap,
|
||||
pub(crate) ignore: Gitignore,
|
||||
@@ -44,52 +43,47 @@ impl AppRunner {
|
||||
runner.fill_filemap(krate);
|
||||
}
|
||||
|
||||
// Ensure the session cache dir exists and is empty
|
||||
runner.flush_session_cache();
|
||||
|
||||
runner
|
||||
}
|
||||
|
||||
pub(crate) async fn wait(&mut self) -> ServeUpdate {
|
||||
// If there are no running apps, we can just return pending to avoid deadlocking
|
||||
if self.running.is_empty() {
|
||||
let Some(handle) = self.running.as_mut() else {
|
||||
return futures_util::future::pending().await;
|
||||
}
|
||||
};
|
||||
|
||||
self.running
|
||||
.iter_mut()
|
||||
.map(|(platform, handle)| async {
|
||||
use ServeUpdate::*;
|
||||
let platform = *platform;
|
||||
tokio::select! {
|
||||
Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stdout.as_mut().map(|f| f.next_line())) => {
|
||||
StdoutReceived { platform, msg }
|
||||
},
|
||||
Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stderr.as_mut().map(|f| f.next_line())) => {
|
||||
StderrReceived { platform, msg }
|
||||
},
|
||||
Some(status) = OptionFuture::from(handle.app_child.as_mut().map(|f| f.wait())) => {
|
||||
match status {
|
||||
Ok(status) => ProcessExited { status, platform },
|
||||
Err(_err) => todo!("handle error in process joining?"),
|
||||
}
|
||||
}
|
||||
Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stdout.as_mut().map(|f| f.next_line())) => {
|
||||
StdoutReceived { platform: Platform::Server, msg }
|
||||
},
|
||||
Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stderr.as_mut().map(|f| f.next_line())) => {
|
||||
StderrReceived { platform: Platform::Server, msg }
|
||||
},
|
||||
Some(status) = OptionFuture::from(handle.server_child.as_mut().map(|f| f.wait())) => {
|
||||
match status {
|
||||
Ok(status) => ProcessExited { status, platform },
|
||||
Err(_err) => todo!("handle error in process joining?"),
|
||||
}
|
||||
}
|
||||
else => futures_util::future::pending().await
|
||||
use ServeUpdate::*;
|
||||
let platform = handle.app.build.build.platform();
|
||||
tokio::select! {
|
||||
Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stdout.as_mut().map(|f| f.next_line())) => {
|
||||
StdoutReceived { platform, msg }
|
||||
},
|
||||
Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stderr.as_mut().map(|f| f.next_line())) => {
|
||||
StderrReceived { platform, msg }
|
||||
},
|
||||
Some(status) = OptionFuture::from(handle.app_child.as_mut().map(|f| f.wait())) => {
|
||||
match status {
|
||||
Ok(status) => ProcessExited { status, platform },
|
||||
Err(_err) => todo!("handle error in process joining?"),
|
||||
}
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.next()
|
||||
.await
|
||||
.expect("Stream to pending if not empty")
|
||||
}
|
||||
Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stdout.as_mut().map(|f| f.next_line())) => {
|
||||
StdoutReceived { platform: Platform::Server, msg }
|
||||
},
|
||||
Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stderr.as_mut().map(|f| f.next_line())) => {
|
||||
StderrReceived { platform: Platform::Server, msg }
|
||||
},
|
||||
Some(status) = OptionFuture::from(handle.server_child.as_mut().map(|f| f.wait())) => {
|
||||
match status {
|
||||
Ok(status) => ProcessExited { status, platform },
|
||||
Err(_err) => todo!("handle error in process joining?"),
|
||||
}
|
||||
}
|
||||
else => futures_util::future::pending().await
|
||||
}
|
||||
}
|
||||
|
||||
/// Finally "bundle" this app and return a handle to it
|
||||
@@ -100,16 +94,9 @@ impl AppRunner {
|
||||
fullstack_address: Option<SocketAddr>,
|
||||
should_open_web: bool,
|
||||
) -> Result<&AppHandle> {
|
||||
let platform = app.build.build.platform();
|
||||
|
||||
// Drop the old handle
|
||||
// todo(jon): we should instead be sending the kill signal rather than dropping the process
|
||||
// This would allow a more graceful shutdown and fix bugs like desktop not retaining its size
|
||||
self.kill(platform).await;
|
||||
|
||||
// wait a tiny sec for the processes to die so we don't have fullstack servers on top of each other
|
||||
// todo(jon): we should allow rebinding to the same port in fullstack itself
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
// This is a more forceful kill than soft_kill since the app entropy will be wiped
|
||||
self.cleanup().await;
|
||||
|
||||
// Add some cute logging
|
||||
if self.builds_opened == 0 {
|
||||
@@ -132,81 +119,32 @@ impl AppRunner {
|
||||
.await?;
|
||||
|
||||
self.builds_opened += 1;
|
||||
self.running.insert(platform, handle);
|
||||
self.running = Some(handle);
|
||||
|
||||
Ok(self.running.get(&platform).unwrap())
|
||||
}
|
||||
|
||||
/// Gracefully kill the process and all of its children
|
||||
///
|
||||
/// Uses the `SIGTERM` signal on unix and `taskkill` on windows.
|
||||
/// This complex logic is necessary for things like window state preservation to work properly.
|
||||
pub(crate) async fn kill(&mut self, platform: Platform) {
|
||||
use tokio::process::Command;
|
||||
|
||||
let Some(mut process) = self.running.remove(&platform) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let server_process = process.server_child.take();
|
||||
let client_process = process.app_child.take();
|
||||
let processes = [server_process, client_process]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for mut process in processes {
|
||||
let Some(pid) = process.id() else {
|
||||
_ = process.kill().await;
|
||||
continue;
|
||||
};
|
||||
|
||||
// on unix, we can send a signal to the process to shut down
|
||||
#[cfg(unix)]
|
||||
{
|
||||
_ = Command::new("kill")
|
||||
.args(["-s", "TERM", &pid.to_string()])
|
||||
.spawn();
|
||||
}
|
||||
|
||||
// on windows, use the `taskkill` command
|
||||
#[cfg(windows)]
|
||||
{
|
||||
_ = Command::new("taskkill")
|
||||
.args(["/F", "/PID", &pid.to_string()])
|
||||
.spawn();
|
||||
}
|
||||
|
||||
// join the wait with a 100ms timeout
|
||||
futures_util::select! {
|
||||
_ = process.wait().fuse() => {}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_millis(1000)).fuse() => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn kill_all(&mut self) {
|
||||
let keys = self.running.keys().cloned().collect::<Vec<_>>();
|
||||
for platform in keys {
|
||||
self.kill(platform).await;
|
||||
}
|
||||
Ok(self.running.as_ref().unwrap())
|
||||
}
|
||||
|
||||
/// 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(), fullstack_address, true)
|
||||
if let Some(runner) = self.running.as_mut() {
|
||||
runner.soft_kill().await;
|
||||
runner
|
||||
.open(devserver.devserver_address(), fullstack_address, true)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shutdown all the running processes
|
||||
pub(crate) async fn cleanup(&mut self) {
|
||||
if let Some(mut process) = self.running.take() {
|
||||
process.cleanup().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn attempt_hot_reload(
|
||||
&mut self,
|
||||
modified_files: Vec<PathBuf>,
|
||||
@@ -237,7 +175,7 @@ impl AppRunner {
|
||||
}
|
||||
|
||||
// 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(runner) = self.running.as_mut() {
|
||||
if let Some(bundled_name) = runner.hotreload_bundled_asset(&path).await {
|
||||
// todo(jon): don't hardcode this here
|
||||
let asset_relative = PathBuf::from("/assets/").join(bundled_name);
|
||||
@@ -329,27 +267,29 @@ impl AppRunner {
|
||||
}
|
||||
|
||||
pub(crate) async fn client_connected(&mut self) {
|
||||
for (platform, runner) in self.running.iter_mut() {
|
||||
// Assign the runtime asset dir to the runner
|
||||
if *platform == Platform::Ios {
|
||||
// xcrun simctl get_app_container booted com.dioxuslabs
|
||||
let res = tokio::process::Command::new("xcrun")
|
||||
.arg("simctl")
|
||||
.arg("get_app_container")
|
||||
.arg("booted")
|
||||
.arg(runner.app.build.krate.bundle_identifier())
|
||||
.output()
|
||||
.await;
|
||||
let Some(handle) = self.running.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(res) = res {
|
||||
tracing::trace!("Using runtime asset dir: {:?}", res);
|
||||
// Assign the runtime asset dir to the runner
|
||||
if handle.app.build.build.platform() == Platform::Ios {
|
||||
// xcrun simctl get_app_container booted com.dioxuslabs
|
||||
let res = tokio::process::Command::new("xcrun")
|
||||
.arg("simctl")
|
||||
.arg("get_app_container")
|
||||
.arg("booted")
|
||||
.arg(handle.app.build.krate.bundle_identifier())
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if let Ok(out) = String::from_utf8(res.stdout) {
|
||||
let out = out.trim();
|
||||
if let Ok(res) = res {
|
||||
tracing::trace!("Using runtime asset dir: {:?}", res);
|
||||
|
||||
tracing::trace!("Setting Runtime asset dir: {out:?}");
|
||||
runner.runtime_asst_dir = Some(PathBuf::from(out));
|
||||
}
|
||||
if let Ok(out) = String::from_utf8(res.stdout) {
|
||||
let out = out.trim();
|
||||
|
||||
tracing::trace!("Setting Runtime asset dir: {out:?}");
|
||||
handle.runtime_asst_dir = Some(PathBuf::from(out));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,4 +328,10 @@ impl AppRunner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_session_cache(&self) {
|
||||
let cache_dir = self.krate.session_cache_dir();
|
||||
_ = std::fs::remove_dir_all(&cache_dir);
|
||||
_ = std::fs::create_dir_all(&cache_dir);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user