handle.rs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. use crate::{AppBundle, DioxusCrate, Platform, Result};
  2. use anyhow::Context;
  3. use dioxus_cli_opt::process_file_to;
  4. use std::{
  5. net::SocketAddr,
  6. path::{Path, PathBuf},
  7. process::Stdio,
  8. };
  9. use tokio::{
  10. io::{AsyncBufReadExt, BufReader, Lines},
  11. process::{Child, ChildStderr, ChildStdout, Command},
  12. };
  13. /// A handle to a running app.
  14. ///
  15. /// Also includes a handle to its server if it exists.
  16. /// The actual child processes might not be present (web) or running (died/killed).
  17. ///
  18. /// The purpose of this struct is to accumulate state about the running app and its server, like
  19. /// any runtime information needed to hotreload the app or send it messages.
  20. ///
  21. /// We might want to bring in websockets here too, so we know the exact channels the app is using to
  22. /// communicate with the devserver. Currently that's a broadcast-type system, so this struct isn't super
  23. /// duper useful.
  24. ///
  25. /// todo: restructure this such that "open" is a running task instead of blocking the main thread
  26. pub(crate) struct AppHandle {
  27. pub(crate) app: AppBundle,
  28. // These might be None if the app died or the user did not specify a server
  29. pub(crate) app_child: Option<Child>,
  30. pub(crate) server_child: Option<Child>,
  31. // stdio for the app so we can read its stdout/stderr
  32. // we don't map stdin today (todo) but most apps don't need it
  33. pub(crate) app_stdout: Option<Lines<BufReader<ChildStdout>>>,
  34. pub(crate) app_stderr: Option<Lines<BufReader<ChildStderr>>>,
  35. pub(crate) server_stdout: Option<Lines<BufReader<ChildStdout>>>,
  36. pub(crate) server_stderr: Option<Lines<BufReader<ChildStderr>>>,
  37. /// The executables but with some extra entropy in their name so we can run two instances of the
  38. /// same app without causing collisions on the filesystem.
  39. pub(crate) entropy_app_exe: Option<PathBuf>,
  40. pub(crate) entropy_server_exe: Option<PathBuf>,
  41. /// The virtual directory that assets will be served from
  42. /// Used mostly for apk/ipa builds since they live in simulator
  43. pub(crate) runtime_asst_dir: Option<PathBuf>,
  44. }
  45. impl AppHandle {
  46. pub async fn new(app: AppBundle) -> Result<Self> {
  47. Ok(AppHandle {
  48. app,
  49. runtime_asst_dir: None,
  50. app_child: None,
  51. app_stderr: None,
  52. app_stdout: None,
  53. server_child: None,
  54. server_stdout: None,
  55. server_stderr: None,
  56. entropy_app_exe: None,
  57. entropy_server_exe: None,
  58. })
  59. }
  60. pub(crate) async fn open(
  61. &mut self,
  62. devserver_ip: SocketAddr,
  63. start_fullstack_on_address: Option<SocketAddr>,
  64. open_browser: bool,
  65. ) -> Result<()> {
  66. let krate = &self.app.build.krate;
  67. // Set the env vars that the clients will expect
  68. // These need to be stable within a release version (ie 0.6.0)
  69. let mut envs = vec![
  70. (dioxus_cli_config::CLI_ENABLED_ENV, "true".to_string()),
  71. (
  72. dioxus_cli_config::ALWAYS_ON_TOP_ENV,
  73. krate.settings.always_on_top.unwrap_or(true).to_string(),
  74. ),
  75. (
  76. dioxus_cli_config::APP_TITLE_ENV,
  77. krate.config.web.app.title.clone(),
  78. ),
  79. ("RUST_BACKTRACE", "1".to_string()),
  80. (
  81. dioxus_cli_config::DEVSERVER_RAW_ADDR_ENV,
  82. devserver_ip.to_string(),
  83. ),
  84. // unset the cargo dirs in the event we're running `dx` locally
  85. // since the child process will inherit the env vars, we don't want to confuse the downstream process
  86. ("CARGO_MANIFEST_DIR", "".to_string()),
  87. (
  88. dioxus_cli_config::SESSION_CACHE_DIR,
  89. self.app
  90. .build
  91. .krate
  92. .session_cache_dir()
  93. .display()
  94. .to_string(),
  95. ),
  96. ];
  97. if let Some(base_path) = &krate.config.web.app.base_path {
  98. envs.push((dioxus_cli_config::ASSET_ROOT_ENV, base_path.clone()));
  99. }
  100. // Launch the server if we were given an address to start it on, and the build includes a server. After we
  101. // start the server, consume its stdout/stderr.
  102. if let (Some(addr), Some(server)) = (start_fullstack_on_address, self.server_exe()) {
  103. tracing::debug!("Proxying fullstack server from port {:?}", addr);
  104. envs.push((dioxus_cli_config::SERVER_IP_ENV, addr.ip().to_string()));
  105. envs.push((dioxus_cli_config::SERVER_PORT_ENV, addr.port().to_string()));
  106. tracing::debug!("Launching server from path: {server:?}");
  107. let mut child = Command::new(server)
  108. .envs(envs.clone())
  109. .stderr(Stdio::piped())
  110. .stdout(Stdio::piped())
  111. .kill_on_drop(true)
  112. .spawn()?;
  113. let stdout = BufReader::new(child.stdout.take().unwrap());
  114. let stderr = BufReader::new(child.stderr.take().unwrap());
  115. self.server_stdout = Some(stdout.lines());
  116. self.server_stderr = Some(stderr.lines());
  117. self.server_child = Some(child);
  118. }
  119. // We try to use stdin/stdout to communicate with the app
  120. let running_process = match self.app.build.build.platform() {
  121. // Unfortunately web won't let us get a proc handle to it (to read its stdout/stderr) so instead
  122. // use use the websocket to communicate with it. I wish we could merge the concepts here,
  123. // like say, opening the socket as a subprocess, but alas, it's simpler to do that somewhere else.
  124. Platform::Web => {
  125. // Only the first build we open the web app, after that the user knows it's running
  126. if open_browser {
  127. self.open_web(devserver_ip);
  128. }
  129. None
  130. }
  131. Platform::Ios => Some(self.open_ios_sim(envs).await?),
  132. // https://developer.android.com/studio/run/emulator-commandline
  133. Platform::Android => {
  134. self.open_android_sim(envs).await;
  135. None
  136. }
  137. // These are all just basically running the main exe, but with slightly different resource dir paths
  138. Platform::Server
  139. | Platform::MacOS
  140. | Platform::Windows
  141. | Platform::Linux
  142. | Platform::Liveview => Some(self.open_with_main_exe(envs)?),
  143. };
  144. // If we have a running process, we need to attach to it and wait for its outputs
  145. if let Some(mut child) = running_process {
  146. let stdout = BufReader::new(child.stdout.take().unwrap());
  147. let stderr = BufReader::new(child.stderr.take().unwrap());
  148. self.app_stdout = Some(stdout.lines());
  149. self.app_stderr = Some(stderr.lines());
  150. self.app_child = Some(child);
  151. }
  152. Ok(())
  153. }
  154. /// Gracefully kill the process and all of its children
  155. ///
  156. /// Uses the `SIGTERM` signal on unix and `taskkill` on windows.
  157. /// This complex logic is necessary for things like window state preservation to work properly.
  158. ///
  159. /// Also wipes away the entropy executables if they exist.
  160. pub(crate) async fn cleanup(&mut self) {
  161. tracing::debug!("Cleaning up process");
  162. // Soft-kill the process by sending a sigkill, allowing the process to clean up
  163. self.soft_kill().await;
  164. // Wipe out the entropy executables if they exist
  165. if let Some(entropy_app_exe) = self.entropy_app_exe.take() {
  166. _ = std::fs::remove_file(entropy_app_exe);
  167. }
  168. if let Some(entropy_server_exe) = self.entropy_server_exe.take() {
  169. _ = std::fs::remove_file(entropy_server_exe);
  170. }
  171. }
  172. /// Kill the app and server exes
  173. pub(crate) async fn soft_kill(&mut self) {
  174. use futures_util::FutureExt;
  175. // Kill any running executables on Windows
  176. let server_process = self.server_child.take();
  177. let client_process = self.app_child.take();
  178. let processes = [server_process, client_process]
  179. .into_iter()
  180. .flatten()
  181. .collect::<Vec<_>>();
  182. for mut process in processes {
  183. let Some(pid) = process.id() else {
  184. _ = process.kill().await;
  185. continue;
  186. };
  187. // on unix, we can send a signal to the process to shut down
  188. #[cfg(unix)]
  189. {
  190. _ = Command::new("kill")
  191. .args(["-s", "TERM", &pid.to_string()])
  192. .spawn();
  193. }
  194. // on windows, use the `taskkill` command
  195. #[cfg(windows)]
  196. {
  197. _ = Command::new("taskkill")
  198. .args(["/F", "/PID", &pid.to_string()])
  199. .spawn();
  200. }
  201. // join the wait with a 100ms timeout
  202. futures_util::select! {
  203. _ = process.wait().fuse() => {}
  204. _ = tokio::time::sleep(std::time::Duration::from_millis(1000)).fuse() => {}
  205. };
  206. }
  207. }
  208. /// Hotreload an asset in the running app.
  209. ///
  210. /// This will modify the build dir in place! Be careful! We generally assume you want all bundles
  211. /// to reflect the latest changes, so we will modify the bundle.
  212. ///
  213. /// However, not all platforms work like this, so we might also need to update a separate asset
  214. /// dir that the system simulator might be providing. We know this is the case for ios simulators
  215. /// and haven't yet checked for android.
  216. ///
  217. /// This will return the bundled name of the asset such that we can send it to the clients letting
  218. /// them know what to reload. It's not super important that this is robust since most clients will
  219. /// kick all stylsheets without necessarily checking the name.
  220. pub(crate) async fn hotreload_bundled_asset(&self, changed_file: &PathBuf) -> Option<PathBuf> {
  221. let mut bundled_name = None;
  222. // Use the build dir if there's no runtime asset dir as the override. For the case of ios apps,
  223. // we won't actually be using the build dir.
  224. let asset_dir = match self.runtime_asst_dir.as_ref() {
  225. Some(dir) => dir.to_path_buf().join("assets/"),
  226. None => self.app.build.asset_dir(),
  227. };
  228. tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
  229. // If the asset shares the same name in the bundle, reload that
  230. if let Some(legacy_asset_dir) = self.app.build.krate.legacy_asset_dir() {
  231. if changed_file.starts_with(&legacy_asset_dir) {
  232. tracing::debug!("Hotreloading legacy asset {changed_file:?}");
  233. let trimmed = changed_file.strip_prefix(legacy_asset_dir).unwrap();
  234. let res = std::fs::copy(changed_file, asset_dir.join(trimmed));
  235. bundled_name = Some(trimmed.to_path_buf());
  236. if let Err(e) = res {
  237. tracing::debug!("Failed to hotreload legacy asset {e}");
  238. }
  239. }
  240. }
  241. // Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\".
  242. let changed_file = dunce::canonicalize(changed_file)
  243. .inspect_err(|e| tracing::debug!("Failed to canonicalize hotreloaded asset: {e}"))
  244. .ok()?;
  245. // The asset might've been renamed thanks to the manifest, let's attempt to reload that too
  246. if let Some(resource) = self.app.app.assets.assets.get(&changed_file).as_ref() {
  247. let output_path = asset_dir.join(resource.bundled_path());
  248. // Remove the old asset if it exists
  249. _ = std::fs::remove_file(&output_path);
  250. // And then process the asset with the options into the **old** asset location. If we recompiled,
  251. // the asset would be in a new location because the contents and hash have changed. Since we are
  252. // hotreloading, we need to use the old asset location it was originally written to.
  253. let options = *resource.options();
  254. let res = process_file_to(&options, &changed_file, &output_path);
  255. bundled_name = Some(PathBuf::from(resource.bundled_path()));
  256. if let Err(e) = res {
  257. tracing::debug!("Failed to hotreload asset {e}");
  258. }
  259. }
  260. // If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
  261. if self.app.build.build.platform() == Platform::Android {
  262. if let Some(bundled_name) = bundled_name.as_ref() {
  263. let target = format!("/data/local/tmp/dx/{}", bundled_name.display());
  264. tracing::debug!("Pushing asset to device: {target}");
  265. let res = tokio::process::Command::new(DioxusCrate::android_adb())
  266. .arg("push")
  267. .arg(&changed_file)
  268. .arg(target)
  269. .output()
  270. .await
  271. .context("Failed to push asset to device");
  272. if let Err(e) = res {
  273. tracing::debug!("Failed to push asset to device: {e}");
  274. }
  275. }
  276. }
  277. // Now we can return the bundled asset name to send to the hotreload engine
  278. bundled_name
  279. }
  280. /// Open the native app simply by running its main exe
  281. ///
  282. /// Eventually, for mac, we want to run the `.app` with `open` to fix issues with `dylib` paths,
  283. /// but for now, we just run the exe directly. Very few users should be caring about `dylib` search
  284. /// paths right now, but they will when we start to enable things like swift integration.
  285. ///
  286. /// Server/liveview/desktop are all basically the same, though
  287. fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
  288. // Create a new entropy app exe if we need to
  289. let main_exe = self.app_exe();
  290. let child = Command::new(main_exe)
  291. .envs(envs)
  292. .stderr(Stdio::piped())
  293. .stdout(Stdio::piped())
  294. .kill_on_drop(true)
  295. .spawn()?;
  296. Ok(child)
  297. }
  298. /// Open the web app by opening the browser to the given address.
  299. /// Check if we need to use https or not, and if so, add the protocol.
  300. /// Go to the basepath if that's set too.
  301. fn open_web(&self, address: SocketAddr) {
  302. let base_path = self.app.build.krate.config.web.app.base_path.clone();
  303. let https = self
  304. .app
  305. .build
  306. .krate
  307. .config
  308. .web
  309. .https
  310. .enabled
  311. .unwrap_or_default();
  312. let protocol = if https { "https" } else { "http" };
  313. let base_path = match base_path.as_deref() {
  314. Some(base_path) => format!("/{}", base_path.trim_matches('/')),
  315. None => "".to_owned(),
  316. };
  317. _ = open::that(format!("{protocol}://{address}{base_path}"));
  318. }
  319. /// Use `xcrun` to install the app to the simulator
  320. /// With simulators, we're free to basically do anything, so we don't need to do any fancy codesigning
  321. /// or entitlements, or anything like that.
  322. ///
  323. /// However, if there's no simulator running, this *might* fail.
  324. ///
  325. /// TODO(jon): we should probably check if there's a simulator running before trying to install,
  326. /// and open the simulator if we have to.
  327. async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result<Child> {
  328. tracing::debug!(
  329. "Installing app to simulator {:?}",
  330. self.app.build.root_dir()
  331. );
  332. let res = Command::new("xcrun")
  333. .arg("simctl")
  334. .arg("install")
  335. .arg("booted")
  336. .arg(self.app.build.root_dir())
  337. .stderr(Stdio::piped())
  338. .stdout(Stdio::piped())
  339. .output()
  340. .await?;
  341. tracing::debug!("Installed app to simulator with exit code: {res:?}");
  342. // Remap the envs to the correct simctl env vars
  343. // iOS sim lets you pass env vars but they need to be in the format "SIMCTL_CHILD_XXX=XXX"
  344. let ios_envs = envs
  345. .iter()
  346. .map(|(k, v)| (format!("SIMCTL_CHILD_{k}"), v.clone()));
  347. let child = Command::new("xcrun")
  348. .arg("simctl")
  349. .arg("launch")
  350. .arg("--console")
  351. .arg("booted")
  352. .arg(self.app.build.krate.bundle_identifier())
  353. .envs(ios_envs)
  354. .stderr(Stdio::piped())
  355. .stdout(Stdio::piped())
  356. .kill_on_drop(true)
  357. .spawn()?;
  358. Ok(child)
  359. }
  360. /// We have this whole thing figured out, but we don't actually use it yet.
  361. ///
  362. /// Launching on devices is more complicated and requires us to codesign the app, which we don't
  363. /// currently do.
  364. ///
  365. /// Converting these commands shouldn't be too hard, but device support would imply we need
  366. /// better support for codesigning and entitlements.
  367. #[allow(unused)]
  368. async fn open_ios_device(&self) -> Result<()> {
  369. use serde_json::Value;
  370. let app_path = self.app.build.root_dir();
  371. install_app(&app_path).await?;
  372. // 2. Determine which device the app was installed to
  373. let device_uuid = get_device_uuid().await?;
  374. // 3. Get the installation URL of the app
  375. let installation_url = get_installation_url(&device_uuid, &app_path).await?;
  376. // 4. Launch the app into the background, paused
  377. launch_app_paused(&device_uuid, &installation_url).await?;
  378. // 5. Pick up the paused app and resume it
  379. resume_app(&device_uuid).await?;
  380. async fn install_app(app_path: &PathBuf) -> Result<()> {
  381. let output = Command::new("xcrun")
  382. .args(["simctl", "install", "booted"])
  383. .arg(app_path)
  384. .output()
  385. .await?;
  386. if !output.status.success() {
  387. return Err(format!("Failed to install app: {:?}", output).into());
  388. }
  389. Ok(())
  390. }
  391. async fn get_device_uuid() -> Result<String> {
  392. let output = Command::new("xcrun")
  393. .args([
  394. "devicectl",
  395. "list",
  396. "devices",
  397. "--json-output",
  398. "target/deviceid.json",
  399. ])
  400. .output()
  401. .await?;
  402. let json: Value =
  403. serde_json::from_str(&std::fs::read_to_string("target/deviceid.json")?)
  404. .context("Failed to parse xcrun output")?;
  405. let device_uuid = json["result"]["devices"][0]["identifier"]
  406. .as_str()
  407. .ok_or("Failed to extract device UUID")?
  408. .to_string();
  409. Ok(device_uuid)
  410. }
  411. async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result<String> {
  412. // xcrun devicectl device install app --device <uuid> --path <path> --json-output
  413. let output = Command::new("xcrun")
  414. .args([
  415. "devicectl",
  416. "device",
  417. "install",
  418. "app",
  419. "--device",
  420. device_uuid,
  421. &app_path.display().to_string(),
  422. "--json-output",
  423. "target/xcrun.json",
  424. ])
  425. .output()
  426. .await?;
  427. if !output.status.success() {
  428. return Err(format!("Failed to install app: {:?}", output).into());
  429. }
  430. let json: Value = serde_json::from_str(&std::fs::read_to_string("target/xcrun.json")?)
  431. .context("Failed to parse xcrun output")?;
  432. let installation_url = json["result"]["installedApplications"][0]["installationURL"]
  433. .as_str()
  434. .ok_or("Failed to extract installation URL")?
  435. .to_string();
  436. Ok(installation_url)
  437. }
  438. async fn launch_app_paused(device_uuid: &str, installation_url: &str) -> Result<()> {
  439. let output = Command::new("xcrun")
  440. .args([
  441. "devicectl",
  442. "device",
  443. "process",
  444. "launch",
  445. "--no-activate",
  446. "--verbose",
  447. "--device",
  448. device_uuid,
  449. installation_url,
  450. "--json-output",
  451. "target/launch.json",
  452. ])
  453. .output()
  454. .await?;
  455. if !output.status.success() {
  456. return Err(format!("Failed to launch app: {:?}", output).into());
  457. }
  458. Ok(())
  459. }
  460. async fn resume_app(device_uuid: &str) -> Result<()> {
  461. let json: Value = serde_json::from_str(&std::fs::read_to_string("target/launch.json")?)
  462. .context("Failed to parse xcrun output")?;
  463. let status_pid = json["result"]["process"]["processIdentifier"]
  464. .as_u64()
  465. .ok_or("Failed to extract process identifier")?;
  466. let output = Command::new("xcrun")
  467. .args([
  468. "devicectl",
  469. "device",
  470. "process",
  471. "resume",
  472. "--device",
  473. device_uuid,
  474. "--pid",
  475. &status_pid.to_string(),
  476. ])
  477. .output()
  478. .await?;
  479. if !output.status.success() {
  480. return Err(format!("Failed to resume app: {:?}", output).into());
  481. }
  482. Ok(())
  483. }
  484. unimplemented!("dioxus-cli doesn't support ios devices yet.")
  485. }
  486. #[allow(unused)]
  487. async fn codesign_ios(&self) -> Result<()> {
  488. const CODESIGN_ERROR: &str = r#"This is likely because you haven't
  489. - Created a provisioning profile before
  490. - Accepted the Apple Developer Program License Agreement
  491. The agreement changes frequently and might need to be accepted again.
  492. To accept the agreement, go to https://developer.apple.com/account
  493. To create a provisioning profile, follow the instructions here:
  494. https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certificates"#;
  495. let profiles_folder = dirs::home_dir()
  496. .context("Your machine has no home-dir")?
  497. .join("Library/MobileDevice/Provisioning Profiles");
  498. if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() {
  499. tracing::error!(
  500. r#"No provisioning profiles found when trying to codesign the app.
  501. We checked the folder: {}
  502. {CODESIGN_ERROR}
  503. "#,
  504. profiles_folder.display()
  505. )
  506. }
  507. let identities = Command::new("security")
  508. .args(["find-identity", "-v", "-p", "codesigning"])
  509. .output()
  510. .await
  511. .context("Failed to run `security find-identity -v -p codesigning`")
  512. .map(|e| {
  513. String::from_utf8(e.stdout)
  514. .context("Failed to parse `security find-identity -v -p codesigning`")
  515. })??;
  516. // Parsing this:
  517. // 51ADE4986E0033A5DB1C794E0D1473D74FD6F871 "Apple Development: jkelleyrtp@gmail.com (XYZYZY)"
  518. let app_dev_name = regex::Regex::new(r#""Apple Development: (.+)""#)
  519. .unwrap()
  520. .captures(&identities)
  521. .and_then(|caps| caps.get(1))
  522. .map(|m| m.as_str())
  523. .context(
  524. "Failed to find Apple Development in `security find-identity -v -p codesigning`",
  525. )?;
  526. // Acquire the provision file
  527. let provision_file = profiles_folder
  528. .read_dir()?
  529. .flatten()
  530. .find(|entry| {
  531. entry
  532. .file_name()
  533. .to_str()
  534. .map(|s| s.contains("mobileprovision"))
  535. .unwrap_or_default()
  536. })
  537. .context("Failed to find a provisioning profile. \n\n{CODESIGN_ERROR}")?;
  538. // The .mobileprovision file has some random binary thrown into into, but it's still basically a plist
  539. // Let's use the plist markers to find the start and end of the plist
  540. fn cut_plist(bytes: &[u8], byte_match: &[u8]) -> Option<usize> {
  541. bytes
  542. .windows(byte_match.len())
  543. .enumerate()
  544. .rev()
  545. .find(|(_, slice)| *slice == byte_match)
  546. .map(|(i, _)| i + byte_match.len())
  547. }
  548. let bytes = std::fs::read(provision_file.path())?;
  549. let cut1 = cut_plist(&bytes, b"<plist").context("Failed to parse .mobileprovision file")?;
  550. let cut2 = cut_plist(&bytes, r#"</dict>"#.as_bytes())
  551. .context("Failed to parse .mobileprovision file")?;
  552. let sub_bytes = &bytes[(cut1 - 6)..cut2];
  553. let mbfile: ProvisioningProfile =
  554. plist::from_bytes(sub_bytes).context("Failed to parse .mobileprovision file")?;
  555. #[derive(serde::Deserialize, Debug)]
  556. struct ProvisioningProfile {
  557. #[serde(rename = "TeamIdentifier")]
  558. team_identifier: Vec<String>,
  559. #[serde(rename = "ApplicationIdentifierPrefix")]
  560. application_identifier_prefix: Vec<String>,
  561. #[serde(rename = "Entitlements")]
  562. entitlements: Entitlements,
  563. }
  564. #[derive(serde::Deserialize, Debug)]
  565. struct Entitlements {
  566. #[serde(rename = "application-identifier")]
  567. application_identifier: String,
  568. #[serde(rename = "keychain-access-groups")]
  569. keychain_access_groups: Vec<String>,
  570. }
  571. let entielements_xml = format!(
  572. r#"
  573. <?xml version="1.0" encoding="UTF-8"?>
  574. <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  575. <plist version="1.0"><dict>
  576. <key>application-identifier</key>
  577. <string>{APPLICATION_IDENTIFIER}</string>
  578. <key>keychain-access-groups</key>
  579. <array>
  580. <string>{APP_ID_ACCESS_GROUP}.*</string>
  581. </array>
  582. <key>get-task-allow</key>
  583. <true/>
  584. <key>com.apple.developer.team-identifier</key>
  585. <string>{TEAM_IDENTIFIER}</string>
  586. </dict></plist>
  587. "#,
  588. APPLICATION_IDENTIFIER = mbfile.entitlements.application_identifier,
  589. APP_ID_ACCESS_GROUP = mbfile.entitlements.keychain_access_groups[0],
  590. TEAM_IDENTIFIER = mbfile.team_identifier[0],
  591. );
  592. // write to a temp file
  593. let temp_file = tempfile::NamedTempFile::new()?;
  594. std::fs::write(temp_file.path(), entielements_xml)?;
  595. // codesign the app
  596. let output = Command::new("codesign")
  597. .args([
  598. "--force",
  599. "--entitlements",
  600. temp_file.path().to_str().unwrap(),
  601. "--sign",
  602. app_dev_name,
  603. ])
  604. .arg(self.app.build.root_dir())
  605. .output()
  606. .await
  607. .context("Failed to codesign the app")?;
  608. if !output.status.success() {
  609. let stderr = String::from_utf8(output.stderr).unwrap_or_default();
  610. return Err(format!("Failed to codesign the app: {stderr}").into());
  611. }
  612. Ok(())
  613. }
  614. async fn open_android_sim(&self, envs: Vec<(&'static str, String)>) {
  615. let apk_path = self.app.apk_path();
  616. let full_mobile_app_name = self.app.build.krate.full_mobile_app_name();
  617. // Start backgrounded since .open() is called while in the arm of the top-level match
  618. tokio::task::spawn(async move {
  619. // Install
  620. // adb install -r app-debug.apk
  621. if let Err(e) = Command::new(DioxusCrate::android_adb())
  622. .arg("install")
  623. .arg("-r")
  624. .arg(apk_path)
  625. .stderr(Stdio::piped())
  626. .stdout(Stdio::piped())
  627. .output()
  628. .await
  629. {
  630. tracing::error!("Failed to install apk with `adb`: {e}");
  631. };
  632. // eventually, use the user's MainAcitivty, not our MainAcitivty
  633. // adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity
  634. let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,);
  635. if let Err(e) = Command::new(DioxusCrate::android_adb())
  636. .arg("shell")
  637. .arg("am")
  638. .arg("start")
  639. .arg("-n")
  640. .arg(activity_name)
  641. .envs(envs)
  642. .stderr(Stdio::piped())
  643. .stdout(Stdio::piped())
  644. .output()
  645. .await
  646. {
  647. tracing::error!("Failed to start app with `adb`: {e}");
  648. };
  649. });
  650. }
  651. fn make_entropy_path(exe: &PathBuf) -> PathBuf {
  652. let id = uuid::Uuid::new_v4();
  653. let name = id.to_string();
  654. let some_entropy = name.split('-').next().unwrap();
  655. // Make a copy of the server exe with a new name
  656. let entropy_server_exe = exe.with_file_name(format!(
  657. "{}-{}",
  658. exe.file_name().unwrap().to_str().unwrap(),
  659. some_entropy
  660. ));
  661. std::fs::copy(exe, &entropy_server_exe).unwrap();
  662. entropy_server_exe
  663. }
  664. fn server_exe(&mut self) -> Option<PathBuf> {
  665. let mut server = self.app.server_exe()?;
  666. // Create a new entropy server exe if we need to
  667. if cfg!(target_os = "windows") || cfg!(target_os = "linux") {
  668. // If we already have an entropy server exe, return it - this is useful for re-opening the same app
  669. if let Some(existing_server) = self.entropy_server_exe.clone() {
  670. return Some(existing_server);
  671. }
  672. // Otherwise, create a new entropy server exe and save it for re-opning
  673. let entropy_server_exe = Self::make_entropy_path(&server);
  674. self.entropy_server_exe = Some(entropy_server_exe.clone());
  675. server = entropy_server_exe;
  676. }
  677. Some(server)
  678. }
  679. fn app_exe(&mut self) -> PathBuf {
  680. let mut main_exe = self.app.main_exe();
  681. // The requirement here is based on the platform, not necessarily our current architecture.
  682. let requires_entropy = match self.app.build.build.platform() {
  683. // When running "bundled", we don't need entropy
  684. Platform::Web => false,
  685. Platform::MacOS => false,
  686. Platform::Ios => false,
  687. Platform::Android => false,
  688. // But on platforms that aren't running as "bundled", we do.
  689. Platform::Windows => true,
  690. Platform::Linux => true,
  691. Platform::Server => true,
  692. Platform::Liveview => true,
  693. };
  694. if requires_entropy || std::env::var("DIOXUS_ENTROPY").is_ok() {
  695. // If we already have an entropy app exe, return it - this is useful for re-opening the same app
  696. if let Some(existing_app_exe) = self.entropy_app_exe.clone() {
  697. return existing_app_exe;
  698. }
  699. let entropy_app_exe = Self::make_entropy_path(&main_exe);
  700. self.entropy_app_exe = Some(entropy_app_exe.clone());
  701. main_exe = entropy_app_exe;
  702. }
  703. main_exe
  704. }
  705. }