dev.rs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. // Copyright 2019-2021 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, AppUrl, WindowUrl},
  9. },
  10. interface::{AppInterface, ExitReason, Interface},
  11. Result,
  12. };
  13. use clap::Parser;
  14. use anyhow::Context;
  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. io::Write,
  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, multiple_occurrences(true), multiple_values(true))]
  43. pub features: Option<Vec<String>>,
  44. /// Exit on panic
  45. #[clap(short, long)]
  46. 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
  54. pub args: Vec<String>,
  55. }
  56. pub fn command(options: Options) -> Result<()> {
  57. let r = command_internal(options);
  58. if r.is_err() {
  59. kill_before_dev_process();
  60. #[cfg(not(debug_assertions))]
  61. let _ = check_for_updates();
  62. }
  63. r
  64. }
  65. fn command_internal(mut options: Options) -> Result<()> {
  66. let tauri_path = tauri_dir();
  67. options.config = if let Some(config) = &options.config {
  68. Some(if config.starts_with('{') {
  69. config.to_string()
  70. } else {
  71. std::fs::read_to_string(&config).with_context(|| "failed to read custom configuration")?
  72. })
  73. } else {
  74. None
  75. };
  76. set_current_dir(&tauri_path).with_context(|| "failed to change current working directory")?;
  77. let config = get_config(options.config.as_deref())?;
  78. if let Some(before_dev) = &config
  79. .lock()
  80. .unwrap()
  81. .as_ref()
  82. .unwrap()
  83. .build
  84. .before_dev_command
  85. {
  86. if !before_dev.is_empty() {
  87. info!(action = "Running"; "BeforeDevCommand (`{}`)", before_dev);
  88. #[cfg(target_os = "windows")]
  89. let mut command = {
  90. let mut command = Command::new("cmd");
  91. command
  92. .arg("/S")
  93. .arg("/C")
  94. .arg(before_dev)
  95. .current_dir(app_dir())
  96. .envs(command_env(true));
  97. command
  98. };
  99. #[cfg(not(target_os = "windows"))]
  100. let mut command = {
  101. let mut command = Command::new("sh");
  102. command
  103. .arg("-c")
  104. .arg(before_dev)
  105. .current_dir(app_dir())
  106. .envs(command_env(true));
  107. command
  108. };
  109. command.stdin(Stdio::piped());
  110. command.stdout(os_pipe::dup_stdout()?);
  111. command.stderr(os_pipe::dup_stderr()?);
  112. let child = SharedChild::spawn(&mut command)
  113. .unwrap_or_else(|_| panic!("failed to run `{}`", before_dev));
  114. let child = Arc::new(child);
  115. let child_ = child.clone();
  116. std::thread::spawn(move || {
  117. let status = child_
  118. .wait()
  119. .expect("failed to wait on \"beforeDevCommand\"");
  120. if !(status.success() || KILL_BEFORE_DEV_FLAG.get().unwrap().load(Ordering::Relaxed)) {
  121. error!("The \"beforeDevCommand\" terminated with a non-zero status code.");
  122. exit(status.code().unwrap_or(1));
  123. }
  124. });
  125. BEFORE_DEV.set(Mutex::new(child)).unwrap();
  126. KILL_BEFORE_DEV_FLAG.set(AtomicBool::default()).unwrap();
  127. let _ = ctrlc::set_handler(move || {
  128. kill_before_dev_process();
  129. #[cfg(not(debug_assertions))]
  130. let _ = check_for_updates();
  131. exit(130);
  132. });
  133. }
  134. }
  135. if options.runner.is_none() {
  136. options.runner = config
  137. .lock()
  138. .unwrap()
  139. .as_ref()
  140. .unwrap()
  141. .build
  142. .runner
  143. .clone();
  144. }
  145. let mut cargo_features = config
  146. .lock()
  147. .unwrap()
  148. .as_ref()
  149. .unwrap()
  150. .build
  151. .features
  152. .clone()
  153. .unwrap_or_default();
  154. if let Some(features) = &options.features {
  155. cargo_features.extend(features.clone());
  156. }
  157. if std::env::var_os("TAURI_SKIP_DEVSERVER_CHECK") != Some("true".into()) {
  158. if let AppUrl::Url(WindowUrl::External(dev_server_url)) = config
  159. .lock()
  160. .unwrap()
  161. .as_ref()
  162. .unwrap()
  163. .build
  164. .dev_path
  165. .clone()
  166. {
  167. let host = dev_server_url
  168. .host()
  169. .unwrap_or_else(|| panic!("No host name in the URL"));
  170. let port = dev_server_url
  171. .port_or_known_default()
  172. .unwrap_or_else(|| panic!("No port number in the URL"));
  173. let addrs;
  174. let addr;
  175. let addrs = match host {
  176. url::Host::Domain(domain) => {
  177. use std::net::ToSocketAddrs;
  178. addrs = (domain, port).to_socket_addrs()?;
  179. addrs.as_slice()
  180. }
  181. url::Host::Ipv4(ip) => {
  182. addr = (ip, port).into();
  183. std::slice::from_ref(&addr)
  184. }
  185. url::Host::Ipv6(ip) => {
  186. addr = (ip, port).into();
  187. std::slice::from_ref(&addr)
  188. }
  189. };
  190. let mut i = 0;
  191. let sleep_interval = std::time::Duration::from_secs(2);
  192. let max_attempts = 90;
  193. loop {
  194. if std::net::TcpStream::connect(addrs).is_ok() {
  195. break;
  196. }
  197. if i % 3 == 0 {
  198. warn!(
  199. "Waiting for your frontend dev server to start on {}...",
  200. dev_server_url
  201. );
  202. }
  203. i += 1;
  204. if i == max_attempts {
  205. error!(
  206. "Could not connect to `{}` after {}s. Please make sure that is the URL to your dev server.",
  207. dev_server_url, i * sleep_interval.as_secs()
  208. );
  209. exit(1);
  210. }
  211. std::thread::sleep(sleep_interval);
  212. }
  213. }
  214. }
  215. let mut interface = AppInterface::new(config.lock().unwrap().as_ref().unwrap())?;
  216. let exit_on_panic = options.exit_on_panic;
  217. interface.dev(options.into(), move |status, reason| {
  218. on_dev_exit(status, reason, exit_on_panic)
  219. })
  220. }
  221. fn on_dev_exit(status: ExitStatus, reason: ExitReason, exit_on_panic: bool) {
  222. if !matches!(reason, ExitReason::TriggeredKill)
  223. && (exit_on_panic || matches!(reason, ExitReason::NormalExit))
  224. {
  225. kill_before_dev_process();
  226. #[cfg(not(debug_assertions))]
  227. let _ = check_for_updates();
  228. exit(status.code().unwrap_or(0));
  229. }
  230. }
  231. #[cfg(not(debug_assertions))]
  232. fn check_for_updates() -> Result<()> {
  233. if std::env::var_os("TAURI_SKIP_UPDATE_CHECK") != Some("true".into()) {
  234. let current_version = crate::info::cli_current_version()?;
  235. let current = semver::Version::parse(&current_version)?;
  236. let upstream_version = crate::info::cli_upstream_version()?;
  237. let upstream = semver::Version::parse(&upstream_version)?;
  238. if current < upstream {
  239. println!(
  240. "🚀 A new version of Tauri CLI is available! [{}]",
  241. upstream.to_string()
  242. );
  243. };
  244. }
  245. Ok(())
  246. }
  247. fn kill_before_dev_process() {
  248. if let Some(child) = BEFORE_DEV.get() {
  249. let child = child.lock().unwrap();
  250. KILL_BEFORE_DEV_FLAG
  251. .get()
  252. .unwrap()
  253. .store(true, Ordering::Relaxed);
  254. #[cfg(windows)]
  255. let _ = Command::new("powershell")
  256. .arg("-NoProfile")
  257. .arg("-Command")
  258. .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()))
  259. .status();
  260. #[cfg(unix)]
  261. {
  262. let mut kill_children_script_path = std::env::temp_dir();
  263. kill_children_script_path.push("kill-children.sh");
  264. if !kill_children_script_path.exists() {
  265. if let Ok(mut file) = std::fs::File::create(&kill_children_script_path) {
  266. use std::os::unix::fs::PermissionsExt;
  267. let _ = file.write_all(KILL_CHILDREN_SCRIPT);
  268. let mut permissions = file.metadata().unwrap().permissions();
  269. permissions.set_mode(0o770);
  270. let _ = file.set_permissions(permissions);
  271. }
  272. }
  273. let _ = Command::new(&kill_children_script_path)
  274. .arg(child.id().to_string())
  275. .output();
  276. }
  277. let _ = child.kill();
  278. }
  279. }