dev.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. // Copyright 2019-2022 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::{get as get_config, reload as reload_config, AppUrl, BeforeDevCommand, WindowUrl},
  9. },
  10. interface::{AppInterface, DevProcess, ExitReason, Interface},
  11. CommandExt, Result,
  12. };
  13. use anyhow::{bail, Context};
  14. use clap::{ArgAction, Parser};
  15. use log::{error, info, warn};
  16. use once_cell::sync::OnceCell;
  17. use shared_child::SharedChild;
  18. use std::{
  19. env::set_current_dir,
  20. net::{IpAddr, Ipv4Addr, SocketAddr},
  21. process::{exit, Command, ExitStatus, Stdio},
  22. sync::{
  23. atomic::{AtomicBool, Ordering},
  24. Arc, Mutex,
  25. },
  26. };
  27. static BEFORE_DEV: OnceCell<Mutex<Arc<SharedChild>>> = OnceCell::new();
  28. static KILL_BEFORE_DEV_FLAG: OnceCell<AtomicBool> = OnceCell::new();
  29. #[cfg(unix)]
  30. const KILL_CHILDREN_SCRIPT: &[u8] = include_bytes!("../scripts/kill-children.sh");
  31. pub const TAURI_DEV_WATCHER_GITIGNORE: &[u8] = include_bytes!("../tauri-dev-watcher.gitignore");
  32. #[derive(Debug, Clone, Parser)]
  33. #[clap(about = "Tauri dev", trailing_var_arg(true))]
  34. pub struct Options {
  35. /// Binary to use to run the application
  36. #[clap(short, long)]
  37. pub runner: Option<String>,
  38. /// Target triple to build against
  39. #[clap(short, long)]
  40. pub target: Option<String>,
  41. /// List of cargo features to activate
  42. #[clap(short, long, action = ArgAction::Append, num_args(0..))]
  43. pub features: Option<Vec<String>>,
  44. /// Exit on panic
  45. #[clap(short, long)]
  46. pub exit_on_panic: bool,
  47. /// JSON string or path to JSON file to merge with tauri.conf.json
  48. #[clap(short, long)]
  49. pub config: Option<String>,
  50. /// Run the code in release mode
  51. #[clap(long = "release")]
  52. pub release_mode: bool,
  53. /// Command line arguments passed to the runner. Arguments after `--` are passed to the application.
  54. pub args: Vec<String>,
  55. /// Disable the file watcher
  56. #[clap(long)]
  57. pub no_watch: bool,
  58. /// Disable the dev server for static files.
  59. #[clap(long)]
  60. pub no_dev_server: bool,
  61. }
  62. pub fn command(options: Options) -> Result<()> {
  63. let r = command_internal(options);
  64. if r.is_err() {
  65. kill_before_dev_process();
  66. #[cfg(not(debug_assertions))]
  67. let _ = check_for_updates();
  68. }
  69. r
  70. }
  71. fn command_internal(mut options: Options) -> Result<()> {
  72. let mut interface = setup(&mut options, false)?;
  73. let exit_on_panic = options.exit_on_panic;
  74. let no_watch = options.no_watch;
  75. interface.dev(options.into(), move |status, reason| {
  76. on_app_exit(status, reason, exit_on_panic, no_watch)
  77. })
  78. }
  79. fn local_ip_address() -> &'static IpAddr {
  80. static LOCAL_IP: OnceCell<IpAddr> = OnceCell::new();
  81. LOCAL_IP.get_or_init(|| {
  82. let addresses: Vec<IpAddr> = local_ip_address::list_afinet_netifas()
  83. .expect("failed to list networks")
  84. .into_iter()
  85. .map(|(_, ipaddr)| ipaddr)
  86. .filter(|ipaddr| match ipaddr {
  87. IpAddr::V4(i) => i != &Ipv4Addr::LOCALHOST,
  88. _ => false,
  89. })
  90. .collect();
  91. match addresses.len() {
  92. 0 => panic!("No external IP detected."),
  93. 1 => {
  94. let ipaddr = addresses.first().unwrap();
  95. log::info!("Detected external IP {ipaddr}.");
  96. *ipaddr
  97. }
  98. _ => {
  99. let selected = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
  100. .with_prompt("What external IP should we use for your development server?")
  101. .items(&addresses)
  102. .default(0)
  103. .interact()
  104. .expect("failed to select external IP");
  105. *addresses.get(selected).unwrap()
  106. }
  107. }
  108. })
  109. }
  110. pub fn setup(options: &mut Options, mobile: bool) -> Result<AppInterface> {
  111. let tauri_path = tauri_dir();
  112. options.config = if let Some(config) = &options.config {
  113. Some(if config.starts_with('{') {
  114. config.to_string()
  115. } else {
  116. std::fs::read_to_string(config).with_context(|| "failed to read custom configuration")?
  117. })
  118. } else {
  119. None
  120. };
  121. set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
  122. let config = get_config(options.config.as_deref())?;
  123. let interface = AppInterface::new(
  124. config.lock().unwrap().as_ref().unwrap(),
  125. options.target.clone(),
  126. )?;
  127. let mut dev_path = config
  128. .lock()
  129. .unwrap()
  130. .as_ref()
  131. .unwrap()
  132. .build
  133. .dev_path
  134. .clone();
  135. if mobile {
  136. if let AppUrl::Url(WindowUrl::External(url)) = &mut dev_path {
  137. let localhost = match url.host() {
  138. Some(url::Host::Domain(d)) => d == "localhost",
  139. Some(url::Host::Ipv4(i)) => {
  140. i == std::net::Ipv4Addr::LOCALHOST || i == std::net::Ipv4Addr::UNSPECIFIED
  141. }
  142. _ => false,
  143. };
  144. if localhost {
  145. let ip = local_ip_address();
  146. url.set_host(Some(&ip.to_string())).unwrap();
  147. if let Some(c) = &options.config {
  148. let mut c: tauri_utils::config::Config = serde_json::from_str(c)?;
  149. c.build.dev_path = dev_path.clone();
  150. options.config = Some(serde_json::to_string(&c).unwrap());
  151. } else {
  152. options.config = Some(format!(r#"{{ "build": {{ "devPath": "{}" }} }}"#, url))
  153. }
  154. reload_config(options.config.as_deref())?;
  155. }
  156. }
  157. }
  158. if let Some(before_dev) = config
  159. .lock()
  160. .unwrap()
  161. .as_ref()
  162. .unwrap()
  163. .build
  164. .before_dev_command
  165. .clone()
  166. {
  167. let (script, script_cwd, wait) = match before_dev {
  168. BeforeDevCommand::Script(s) if s.is_empty() => (None, None, false),
  169. BeforeDevCommand::Script(s) => (Some(s), None, false),
  170. BeforeDevCommand::ScriptWithOptions { script, cwd, wait } => {
  171. (Some(script), cwd.map(Into::into), wait)
  172. }
  173. };
  174. let cwd = script_cwd.unwrap_or_else(|| app_dir().clone());
  175. if let Some(mut before_dev) = script {
  176. if before_dev.contains("$HOST") {
  177. if mobile {
  178. let local_ip_address = local_ip_address().to_string();
  179. before_dev = before_dev.replace("$HOST", &local_ip_address);
  180. if let AppUrl::Url(WindowUrl::External(url)) = &mut dev_path {
  181. url.set_host(Some(&local_ip_address))?;
  182. }
  183. } else {
  184. before_dev = before_dev.replace(
  185. "$HOST",
  186. if let AppUrl::Url(WindowUrl::External(url)) = &dev_path {
  187. url.host_str().unwrap_or("0.0.0.0")
  188. } else {
  189. "0.0.0.0"
  190. },
  191. );
  192. }
  193. }
  194. info!(action = "Running"; "BeforeDevCommand (`{}`)", before_dev);
  195. let mut env = command_env(true);
  196. env.extend(interface.env());
  197. #[cfg(windows)]
  198. let mut command = {
  199. let mut command = Command::new("cmd");
  200. command
  201. .arg("/S")
  202. .arg("/C")
  203. .arg(&before_dev)
  204. .current_dir(cwd)
  205. .envs(env);
  206. command
  207. };
  208. #[cfg(not(windows))]
  209. let mut command = {
  210. let mut command = Command::new("sh");
  211. command
  212. .arg("-c")
  213. .arg(&before_dev)
  214. .current_dir(cwd)
  215. .envs(env);
  216. command
  217. };
  218. if wait {
  219. let status = command.piped().with_context(|| {
  220. format!(
  221. "failed to run `{}` with `{}`",
  222. before_dev,
  223. if cfg!(windows) { "cmd /S /C" } else { "sh -c" }
  224. )
  225. })?;
  226. if !status.success() {
  227. bail!(
  228. "beforeDevCommand `{}` failed with exit code {}",
  229. before_dev,
  230. status.code().unwrap_or_default()
  231. );
  232. }
  233. } else {
  234. command.stdin(Stdio::piped());
  235. command.stdout(os_pipe::dup_stdout()?);
  236. command.stderr(os_pipe::dup_stderr()?);
  237. let child = SharedChild::spawn(&mut command)
  238. .unwrap_or_else(|_| panic!("failed to run `{}`", before_dev));
  239. let child = Arc::new(child);
  240. let child_ = child.clone();
  241. std::thread::spawn(move || {
  242. let status = child_
  243. .wait()
  244. .expect("failed to wait on \"beforeDevCommand\"");
  245. if !(status.success() || KILL_BEFORE_DEV_FLAG.get().unwrap().load(Ordering::Relaxed)) {
  246. error!("The \"beforeDevCommand\" terminated with a non-zero status code.");
  247. exit(status.code().unwrap_or(1));
  248. }
  249. });
  250. BEFORE_DEV.set(Mutex::new(child)).unwrap();
  251. KILL_BEFORE_DEV_FLAG.set(AtomicBool::default()).unwrap();
  252. let _ = ctrlc::set_handler(move || {
  253. kill_before_dev_process();
  254. #[cfg(not(debug_assertions))]
  255. let _ = check_for_updates();
  256. exit(130);
  257. });
  258. }
  259. }
  260. }
  261. if options.runner.is_none() {
  262. options.runner = config
  263. .lock()
  264. .unwrap()
  265. .as_ref()
  266. .unwrap()
  267. .build
  268. .runner
  269. .clone();
  270. }
  271. let mut cargo_features = config
  272. .lock()
  273. .unwrap()
  274. .as_ref()
  275. .unwrap()
  276. .build
  277. .features
  278. .clone()
  279. .unwrap_or_default();
  280. if let Some(features) = &options.features {
  281. cargo_features.extend(features.clone());
  282. }
  283. if let AppUrl::Url(WindowUrl::App(path)) = &dev_path {
  284. use crate::helpers::web_dev_server::start_dev_server;
  285. if path.exists() {
  286. let ip = if mobile {
  287. *local_ip_address()
  288. } else {
  289. Ipv4Addr::new(127, 0, 0, 1).into()
  290. };
  291. let port = 1430;
  292. let server_address = SocketAddr::new(ip, port);
  293. let path = path.canonicalize()?;
  294. start_dev_server(server_address, path);
  295. let server_url = format!("http://{}", server_address);
  296. dev_path = AppUrl::Url(WindowUrl::External(server_url.parse().unwrap()));
  297. // TODO: in v2, use an env var to pass the url to the app context
  298. // or better separate the config passed from the cli internally and
  299. // config passed by the user in `--config` into to separate env vars
  300. // and the context merges, the user first, then the internal cli config
  301. if let Some(c) = &options.config {
  302. let mut c: tauri_utils::config::Config = serde_json::from_str(c)?;
  303. c.build.dev_path = dev_path.clone();
  304. options.config = Some(serde_json::to_string(&c).unwrap());
  305. } else {
  306. options.config = Some(format!(
  307. r#"{{ "build": {{ "devPath": "{}" }} }}"#,
  308. server_url
  309. ))
  310. }
  311. }
  312. reload_config(options.config.as_deref())?;
  313. }
  314. if std::env::var_os("TAURI_SKIP_DEVSERVER_CHECK") != Some("true".into()) {
  315. if let AppUrl::Url(WindowUrl::External(dev_server_url)) = dev_path {
  316. let host = dev_server_url
  317. .host()
  318. .unwrap_or_else(|| panic!("No host name in the URL"));
  319. let port = dev_server_url
  320. .port_or_known_default()
  321. .unwrap_or_else(|| panic!("No port number in the URL"));
  322. let addrs;
  323. let addr;
  324. let addrs = match host {
  325. url::Host::Domain(domain) => {
  326. use std::net::ToSocketAddrs;
  327. addrs = (domain, port).to_socket_addrs()?;
  328. addrs.as_slice()
  329. }
  330. url::Host::Ipv4(ip) => {
  331. addr = (ip, port).into();
  332. std::slice::from_ref(&addr)
  333. }
  334. url::Host::Ipv6(ip) => {
  335. addr = (ip, port).into();
  336. std::slice::from_ref(&addr)
  337. }
  338. };
  339. let mut i = 0;
  340. let sleep_interval = std::time::Duration::from_secs(2);
  341. let max_attempts = 90;
  342. loop {
  343. if std::net::TcpStream::connect(addrs).is_ok() {
  344. break;
  345. }
  346. if i % 3 == 1 {
  347. warn!(
  348. "Waiting for your frontend dev server to start on {}...",
  349. dev_server_url
  350. );
  351. }
  352. i += 1;
  353. if i == max_attempts {
  354. error!(
  355. "Could not connect to `{}` after {}s. Please make sure that is the URL to your dev server.",
  356. dev_server_url, i * sleep_interval.as_secs()
  357. );
  358. exit(1);
  359. }
  360. std::thread::sleep(sleep_interval);
  361. }
  362. }
  363. }
  364. Ok(interface)
  365. }
  366. pub fn wait_dev_process<
  367. C: DevProcess + Send + 'static,
  368. F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static,
  369. >(
  370. child: C,
  371. on_exit: F,
  372. ) {
  373. std::thread::spawn(move || {
  374. let status = child.wait().expect("failed to wait on app");
  375. on_exit(
  376. status,
  377. if child.manually_killed_process() {
  378. ExitReason::TriggeredKill
  379. } else {
  380. ExitReason::NormalExit
  381. },
  382. );
  383. });
  384. }
  385. pub fn on_app_exit(status: ExitStatus, reason: ExitReason, exit_on_panic: bool, no_watch: bool) {
  386. if no_watch
  387. || (!matches!(reason, ExitReason::TriggeredKill)
  388. && (exit_on_panic || matches!(reason, ExitReason::NormalExit)))
  389. {
  390. kill_before_dev_process();
  391. #[cfg(not(debug_assertions))]
  392. let _ = check_for_updates();
  393. exit(status.code().unwrap_or(0));
  394. }
  395. }
  396. #[cfg(not(debug_assertions))]
  397. fn check_for_updates() -> Result<()> {
  398. if std::env::var_os("TAURI_SKIP_UPDATE_CHECK") != Some("true".into()) {
  399. let current_version = crate::info::cli_current_version()?;
  400. let current = semver::Version::parse(&current_version)?;
  401. let upstream_version = crate::info::cli_upstream_version()?;
  402. let upstream = semver::Version::parse(&upstream_version)?;
  403. if current < upstream {
  404. println!(
  405. "🚀 A new version of Tauri CLI is available! [{}]",
  406. upstream.to_string()
  407. );
  408. };
  409. }
  410. Ok(())
  411. }
  412. pub fn kill_before_dev_process() {
  413. if let Some(child) = BEFORE_DEV.get() {
  414. let child = child.lock().unwrap();
  415. KILL_BEFORE_DEV_FLAG
  416. .get()
  417. .unwrap()
  418. .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("kill-children.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. }