dev.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use crate::{
  5. helpers::{
  6. app_paths::{app_dir, tauri_dir},
  7. command_env,
  8. config::{
  9. get as get_config, reload as reload_config, BeforeDevCommand, ConfigHandle, FrontendDist,
  10. },
  11. },
  12. interface::{AppInterface, DevProcess, ExitReason, Interface},
  13. CommandExt, ConfigValue, Result,
  14. };
  15. use anyhow::{bail, Context};
  16. use clap::{ArgAction, Parser};
  17. use shared_child::SharedChild;
  18. use tauri_utils::platform::Target;
  19. use std::{
  20. env::set_current_dir,
  21. net::{IpAddr, Ipv4Addr},
  22. process::{exit, Command, Stdio},
  23. sync::{
  24. atomic::{AtomicBool, Ordering},
  25. Arc, Mutex, OnceLock,
  26. },
  27. };
  28. mod builtin_dev_server;
  29. static BEFORE_DEV: OnceLock<Mutex<Arc<SharedChild>>> = OnceLock::new();
  30. static KILL_BEFORE_DEV_FLAG: OnceLock<AtomicBool> = OnceLock::new();
  31. #[cfg(unix)]
  32. const KILL_CHILDREN_SCRIPT: &[u8] = include_bytes!("../scripts/kill-children.sh");
  33. pub const TAURI_CLI_BUILTIN_WATCHER_IGNORE_FILE: &[u8] =
  34. include_bytes!("../tauri-dev-watcher.gitignore");
  35. #[derive(Debug, Clone, Parser)]
  36. #[clap(
  37. about = "Run your app in development mode",
  38. long_about = "Run your app in development mode with hot-reloading for the Rust code. It makes use of the `build.devUrl` property from your `tauri.conf.json` file. It also runs your `build.beforeDevCommand` which usually starts your frontend devServer.",
  39. trailing_var_arg(true)
  40. )]
  41. pub struct Options {
  42. /// Binary to use to run the application
  43. #[clap(short, long)]
  44. pub runner: Option<String>,
  45. /// Target triple to build against
  46. #[clap(short, long)]
  47. pub target: Option<String>,
  48. /// List of cargo features to activate
  49. #[clap(short, long, action = ArgAction::Append, num_args(0..))]
  50. pub features: Option<Vec<String>>,
  51. /// Exit on panic
  52. #[clap(short, long)]
  53. pub exit_on_panic: bool,
  54. /// JSON string or path to JSON file to merge with tauri.conf.json
  55. #[clap(short, long)]
  56. pub config: Option<ConfigValue>,
  57. /// Run the code in release mode
  58. #[clap(long = "release")]
  59. pub release_mode: bool,
  60. /// Command line arguments passed to the runner.
  61. /// Use `--` to explicitly mark the start of the arguments. Arguments after a second `--` are passed to the application
  62. /// e.g. `tauri dev -- [runnerArgs] -- [appArgs]`.
  63. pub args: Vec<String>,
  64. /// Skip waiting for the frontend dev server to start before building the tauri application.
  65. #[clap(long, env = "TAURI_CLI_NO_DEV_SERVER_WAIT")]
  66. pub no_dev_server_wait: bool,
  67. /// Disable the file watcher.
  68. #[clap(long)]
  69. pub no_watch: bool,
  70. /// Force prompting for an IP to use to connect to the dev server on mobile.
  71. #[clap(long)]
  72. pub force_ip_prompt: bool,
  73. /// Disable the built-in dev server for static files.
  74. #[clap(long)]
  75. pub no_dev_server: bool,
  76. /// Specify port for the built-in dev server for static files. Defaults to 1430.
  77. #[clap(long, env = "TAURI_CLI_PORT")]
  78. pub port: Option<u16>,
  79. }
  80. pub fn command(options: Options) -> Result<()> {
  81. let r = command_internal(options);
  82. if r.is_err() {
  83. kill_before_dev_process();
  84. }
  85. r
  86. }
  87. fn command_internal(mut options: Options) -> Result<()> {
  88. let target = options
  89. .target
  90. .as_deref()
  91. .map(Target::from_triple)
  92. .unwrap_or_else(Target::current);
  93. let config = get_config(target, options.config.as_ref().map(|c| &c.0))?;
  94. let mut interface = AppInterface::new(
  95. config.lock().unwrap().as_ref().unwrap(),
  96. options.target.clone(),
  97. )?;
  98. setup(&interface, &mut options, config, false)?;
  99. let exit_on_panic = options.exit_on_panic;
  100. let no_watch = options.no_watch;
  101. interface.dev(options.into(), move |status, reason| {
  102. on_app_exit(status, reason, exit_on_panic, no_watch)
  103. })
  104. }
  105. pub fn local_ip_address(force: bool) -> &'static IpAddr {
  106. static LOCAL_IP: OnceLock<IpAddr> = OnceLock::new();
  107. LOCAL_IP.get_or_init(|| {
  108. let prompt_for_ip = || {
  109. let addresses: Vec<IpAddr> = local_ip_address::list_afinet_netifas()
  110. .expect("failed to list networks")
  111. .into_iter()
  112. .map(|(_, ipaddr)| ipaddr)
  113. .filter(|ipaddr| match ipaddr {
  114. IpAddr::V4(i) => i != &Ipv4Addr::LOCALHOST,
  115. _ => false,
  116. })
  117. .collect();
  118. match addresses.len() {
  119. 0 => panic!("No external IP detected."),
  120. 1 => {
  121. let ipaddr = addresses.first().unwrap();
  122. *ipaddr
  123. }
  124. _ => {
  125. let selected = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
  126. .with_prompt(
  127. "Failed to detect external IP, What IP should we use to access your development server?",
  128. )
  129. .items(&addresses)
  130. .default(0)
  131. .interact()
  132. .expect("failed to select external IP");
  133. *addresses.get(selected).unwrap()
  134. }
  135. }
  136. };
  137. let ip = if force {
  138. prompt_for_ip()
  139. } else {
  140. local_ip_address::local_ip().unwrap_or_else(|_| prompt_for_ip())
  141. };
  142. log::info!("Using {ip} to access the development server.");
  143. ip
  144. })
  145. }
  146. pub fn setup(
  147. interface: &AppInterface,
  148. options: &mut Options,
  149. config: ConfigHandle,
  150. mobile: bool,
  151. ) -> Result<()> {
  152. let tauri_path = tauri_dir();
  153. set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
  154. let mut dev_url = config
  155. .lock()
  156. .unwrap()
  157. .as_ref()
  158. .unwrap()
  159. .build
  160. .dev_url
  161. .clone();
  162. if let Some(before_dev) = config
  163. .lock()
  164. .unwrap()
  165. .as_ref()
  166. .unwrap()
  167. .build
  168. .before_dev_command
  169. .clone()
  170. {
  171. let (script, script_cwd, wait) = match before_dev {
  172. BeforeDevCommand::Script(s) if s.is_empty() => (None, None, false),
  173. BeforeDevCommand::Script(s) => (Some(s), None, false),
  174. BeforeDevCommand::ScriptWithOptions { script, cwd, wait } => {
  175. (Some(script), cwd.map(Into::into), wait)
  176. }
  177. };
  178. let cwd = script_cwd.unwrap_or_else(|| app_dir().clone());
  179. if let Some(mut before_dev) = script {
  180. if before_dev.contains("$HOST") {
  181. if mobile {
  182. let local_ip_address = local_ip_address(options.force_ip_prompt).to_string();
  183. before_dev = before_dev.replace("$HOST", &local_ip_address);
  184. if let Some(url) = &mut dev_url {
  185. url.set_host(Some(&local_ip_address))?;
  186. }
  187. } else {
  188. before_dev = before_dev.replace(
  189. "$HOST",
  190. if let Some(url) = &dev_url {
  191. url.host_str().unwrap_or("127.0.0.1")
  192. } else {
  193. "127.0.0.1"
  194. },
  195. );
  196. }
  197. }
  198. log::info!(action = "Running"; "BeforeDevCommand (`{}`)", before_dev);
  199. let mut env = command_env(true);
  200. env.extend(interface.env());
  201. #[cfg(windows)]
  202. let mut command = {
  203. let mut command = Command::new("cmd");
  204. command
  205. .arg("/S")
  206. .arg("/C")
  207. .arg(&before_dev)
  208. .current_dir(cwd)
  209. .envs(env);
  210. command
  211. };
  212. #[cfg(not(windows))]
  213. let mut command = {
  214. let mut command = Command::new("sh");
  215. command
  216. .arg("-c")
  217. .arg(&before_dev)
  218. .current_dir(cwd)
  219. .envs(env);
  220. command
  221. };
  222. if wait {
  223. let status = command.piped().with_context(|| {
  224. format!(
  225. "failed to run `{}` with `{}`",
  226. before_dev,
  227. if cfg!(windows) { "cmd /S /C" } else { "sh -c" }
  228. )
  229. })?;
  230. if !status.success() {
  231. bail!(
  232. "beforeDevCommand `{}` failed with exit code {}",
  233. before_dev,
  234. status.code().unwrap_or_default()
  235. );
  236. }
  237. } else {
  238. command.stdin(Stdio::piped());
  239. command.stdout(os_pipe::dup_stdout()?);
  240. command.stderr(os_pipe::dup_stderr()?);
  241. let child = SharedChild::spawn(&mut command)
  242. .unwrap_or_else(|_| panic!("failed to run `{before_dev}`"));
  243. let child = Arc::new(child);
  244. let child_ = child.clone();
  245. std::thread::spawn(move || {
  246. let status = child_
  247. .wait()
  248. .expect("failed to wait on \"beforeDevCommand\"");
  249. if !(status.success() || KILL_BEFORE_DEV_FLAG.get().unwrap().load(Ordering::Relaxed)) {
  250. log::error!("The \"beforeDevCommand\" terminated with a non-zero status code.");
  251. exit(status.code().unwrap_or(1));
  252. }
  253. });
  254. BEFORE_DEV.set(Mutex::new(child)).unwrap();
  255. KILL_BEFORE_DEV_FLAG.set(AtomicBool::default()).unwrap();
  256. let _ = ctrlc::set_handler(move || {
  257. kill_before_dev_process();
  258. exit(130);
  259. });
  260. }
  261. }
  262. }
  263. if options.runner.is_none() {
  264. options
  265. .runner
  266. .clone_from(&config.lock().unwrap().as_ref().unwrap().build.runner);
  267. }
  268. let mut cargo_features = config
  269. .lock()
  270. .unwrap()
  271. .as_ref()
  272. .unwrap()
  273. .build
  274. .features
  275. .clone()
  276. .unwrap_or_default();
  277. if let Some(features) = &options.features {
  278. cargo_features.extend(features.clone());
  279. }
  280. let mut dev_url = config
  281. .lock()
  282. .unwrap()
  283. .as_ref()
  284. .unwrap()
  285. .build
  286. .dev_url
  287. .clone();
  288. let frontend_dist = config
  289. .lock()
  290. .unwrap()
  291. .as_ref()
  292. .unwrap()
  293. .build
  294. .frontend_dist
  295. .clone();
  296. if !options.no_dev_server && dev_url.is_none() {
  297. if let Some(FrontendDist::Directory(path)) = &frontend_dist {
  298. if path.exists() {
  299. let path = path.canonicalize()?;
  300. let ip = if mobile {
  301. *local_ip_address(options.force_ip_prompt)
  302. } else {
  303. Ipv4Addr::new(127, 0, 0, 1).into()
  304. };
  305. let server_url = builtin_dev_server::start(path, ip, options.port)?;
  306. let server_url = format!("http://{server_url}");
  307. dev_url = Some(server_url.parse().unwrap());
  308. if let Some(c) = &mut options.config {
  309. if let Some(build) = c
  310. .0
  311. .as_object_mut()
  312. .and_then(|root| root.get_mut("build"))
  313. .and_then(|build| build.as_object_mut())
  314. {
  315. build.insert("devUrl".into(), server_url.into());
  316. }
  317. } else {
  318. options
  319. .config
  320. .replace(crate::ConfigValue(serde_json::json!({
  321. "build": {
  322. "devUrl": server_url
  323. }
  324. })));
  325. }
  326. reload_config(options.config.as_ref().map(|c| &c.0))?;
  327. }
  328. }
  329. }
  330. if !options.no_dev_server_wait {
  331. if let Some(url) = dev_url {
  332. let host = url
  333. .host()
  334. .unwrap_or_else(|| panic!("No host name in the URL"));
  335. let port = url
  336. .port_or_known_default()
  337. .unwrap_or_else(|| panic!("No port number in the URL"));
  338. let addrs;
  339. let addr;
  340. let addrs = match host {
  341. url::Host::Domain(domain) => {
  342. use std::net::ToSocketAddrs;
  343. addrs = (domain, port).to_socket_addrs()?;
  344. addrs.as_slice()
  345. }
  346. url::Host::Ipv4(ip) => {
  347. addr = (ip, port).into();
  348. std::slice::from_ref(&addr)
  349. }
  350. url::Host::Ipv6(ip) => {
  351. addr = (ip, port).into();
  352. std::slice::from_ref(&addr)
  353. }
  354. };
  355. let mut i = 0;
  356. let sleep_interval = std::time::Duration::from_secs(2);
  357. let timeout_duration = std::time::Duration::from_secs(1);
  358. let max_attempts = 90;
  359. 'waiting: loop {
  360. for addr in addrs.iter() {
  361. if std::net::TcpStream::connect_timeout(addr, timeout_duration).is_ok() {
  362. break 'waiting;
  363. }
  364. }
  365. if i % 3 == 1 {
  366. log::warn!("Waiting for your frontend dev server to start on {url}...",);
  367. }
  368. i += 1;
  369. if i == max_attempts {
  370. log::error!("Could not connect to `{url}` after {}s. Please make sure that is the URL to your dev server.", i * sleep_interval.as_secs());
  371. exit(1);
  372. }
  373. std::thread::sleep(sleep_interval);
  374. }
  375. }
  376. }
  377. Ok(())
  378. }
  379. pub fn wait_dev_process<
  380. C: DevProcess + Send + 'static,
  381. F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static,
  382. >(
  383. child: C,
  384. on_exit: F,
  385. ) {
  386. std::thread::spawn(move || {
  387. let code = child
  388. .wait()
  389. .ok()
  390. .and_then(|status| status.code())
  391. .or(Some(1));
  392. on_exit(
  393. code,
  394. if child.manually_killed_process() {
  395. ExitReason::TriggeredKill
  396. } else {
  397. ExitReason::NormalExit
  398. },
  399. );
  400. });
  401. }
  402. pub fn on_app_exit(code: Option<i32>, reason: ExitReason, exit_on_panic: bool, no_watch: bool) {
  403. if no_watch
  404. || (!matches!(reason, ExitReason::TriggeredKill)
  405. && (exit_on_panic || matches!(reason, ExitReason::NormalExit)))
  406. {
  407. kill_before_dev_process();
  408. exit(code.unwrap_or(0));
  409. }
  410. }
  411. pub fn kill_before_dev_process() {
  412. if let Some(child) = BEFORE_DEV.get() {
  413. let child = child.lock().unwrap();
  414. let kill_before_dev_flag = KILL_BEFORE_DEV_FLAG.get().unwrap();
  415. if kill_before_dev_flag.load(Ordering::Relaxed) {
  416. return;
  417. }
  418. kill_before_dev_flag.store(true, Ordering::Relaxed);
  419. #[cfg(windows)]
  420. {
  421. let powershell_path = std::env::var("SYSTEMROOT").map_or_else(
  422. |_| "powershell.exe".to_string(),
  423. |p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
  424. );
  425. let _ = Command::new(powershell_path)
  426. .arg("-NoProfile")
  427. .arg("-Command")
  428. .arg(format!("function Kill-Tree {{ Param([int]$ppid); Get-CimInstance Win32_Process | Where-Object {{ $_.ParentProcessId -eq $ppid }} | ForEach-Object {{ Kill-Tree $_.ProcessId }}; Stop-Process -Id $ppid -ErrorAction SilentlyContinue }}; Kill-Tree {}", child.id()))
  429. .status();
  430. }
  431. #[cfg(unix)]
  432. {
  433. use std::io::Write;
  434. let mut kill_children_script_path = std::env::temp_dir();
  435. kill_children_script_path.push("tauri-stop-dev-processes.sh");
  436. if !kill_children_script_path.exists() {
  437. if let Ok(mut file) = std::fs::File::create(&kill_children_script_path) {
  438. use std::os::unix::fs::PermissionsExt;
  439. let _ = file.write_all(KILL_CHILDREN_SCRIPT);
  440. let mut permissions = file.metadata().unwrap().permissions();
  441. permissions.set_mode(0o770);
  442. let _ = file.set_permissions(permissions);
  443. }
  444. }
  445. let _ = Command::new(&kill_children_script_path)
  446. .arg(child.id().to_string())
  447. .output();
  448. }
  449. let _ = child.kill();
  450. }
  451. }