dev.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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. /// Disable the built-in dev server for static files.
  71. #[clap(long)]
  72. pub no_dev_server: bool,
  73. /// Specify port for the built-in dev server for static files. Defaults to 1430.
  74. #[clap(long, env = "TAURI_CLI_PORT")]
  75. pub port: Option<u16>,
  76. #[clap(skip)]
  77. pub host: Option<IpAddr>,
  78. }
  79. pub fn command(options: Options) -> Result<()> {
  80. crate::helpers::app_paths::resolve();
  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)?;
  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 setup(interface: &AppInterface, options: &mut Options, config: ConfigHandle) -> Result<()> {
  106. let tauri_path = tauri_dir();
  107. set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
  108. if let Some(before_dev) = config
  109. .lock()
  110. .unwrap()
  111. .as_ref()
  112. .unwrap()
  113. .build
  114. .before_dev_command
  115. .clone()
  116. {
  117. let (script, script_cwd, wait) = match before_dev {
  118. BeforeDevCommand::Script(s) if s.is_empty() => (None, None, false),
  119. BeforeDevCommand::Script(s) => (Some(s), None, false),
  120. BeforeDevCommand::ScriptWithOptions { script, cwd, wait } => {
  121. (Some(script), cwd.map(Into::into), wait)
  122. }
  123. };
  124. let cwd = script_cwd.unwrap_or_else(|| app_dir().clone());
  125. if let Some(before_dev) = script {
  126. log::info!(action = "Running"; "BeforeDevCommand (`{}`)", before_dev);
  127. let mut env = command_env(true);
  128. env.extend(interface.env());
  129. #[cfg(windows)]
  130. let mut command = {
  131. let mut command = Command::new("cmd");
  132. command
  133. .arg("/S")
  134. .arg("/C")
  135. .arg(&before_dev)
  136. .current_dir(cwd)
  137. .envs(env);
  138. command
  139. };
  140. #[cfg(not(windows))]
  141. let mut command = {
  142. let mut command = Command::new("sh");
  143. command
  144. .arg("-c")
  145. .arg(&before_dev)
  146. .current_dir(cwd)
  147. .envs(env);
  148. command
  149. };
  150. if wait {
  151. let status = command.piped().with_context(|| {
  152. format!(
  153. "failed to run `{}` with `{}`",
  154. before_dev,
  155. if cfg!(windows) { "cmd /S /C" } else { "sh -c" }
  156. )
  157. })?;
  158. if !status.success() {
  159. bail!(
  160. "beforeDevCommand `{}` failed with exit code {}",
  161. before_dev,
  162. status.code().unwrap_or_default()
  163. );
  164. }
  165. } else {
  166. command.stdin(Stdio::piped());
  167. command.stdout(os_pipe::dup_stdout()?);
  168. command.stderr(os_pipe::dup_stderr()?);
  169. let child = SharedChild::spawn(&mut command)
  170. .unwrap_or_else(|_| panic!("failed to run `{before_dev}`"));
  171. let child = Arc::new(child);
  172. let child_ = child.clone();
  173. std::thread::spawn(move || {
  174. let status = child_
  175. .wait()
  176. .expect("failed to wait on \"beforeDevCommand\"");
  177. if !(status.success() || KILL_BEFORE_DEV_FLAG.get().unwrap().load(Ordering::Relaxed)) {
  178. log::error!("The \"beforeDevCommand\" terminated with a non-zero status code.");
  179. exit(status.code().unwrap_or(1));
  180. }
  181. });
  182. BEFORE_DEV.set(Mutex::new(child)).unwrap();
  183. KILL_BEFORE_DEV_FLAG.set(AtomicBool::default()).unwrap();
  184. let _ = ctrlc::set_handler(move || {
  185. kill_before_dev_process();
  186. exit(130);
  187. });
  188. }
  189. }
  190. }
  191. if options.runner.is_none() {
  192. options
  193. .runner
  194. .clone_from(&config.lock().unwrap().as_ref().unwrap().build.runner);
  195. }
  196. let mut cargo_features = config
  197. .lock()
  198. .unwrap()
  199. .as_ref()
  200. .unwrap()
  201. .build
  202. .features
  203. .clone()
  204. .unwrap_or_default();
  205. if let Some(features) = &options.features {
  206. cargo_features.extend(features.clone());
  207. }
  208. let mut dev_url = config
  209. .lock()
  210. .unwrap()
  211. .as_ref()
  212. .unwrap()
  213. .build
  214. .dev_url
  215. .clone();
  216. let frontend_dist = config
  217. .lock()
  218. .unwrap()
  219. .as_ref()
  220. .unwrap()
  221. .build
  222. .frontend_dist
  223. .clone();
  224. if !options.no_dev_server && dev_url.is_none() {
  225. if let Some(FrontendDist::Directory(path)) = &frontend_dist {
  226. if path.exists() {
  227. let path = path.canonicalize()?;
  228. let ip = options
  229. .host
  230. .unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1).into());
  231. let server_url = builtin_dev_server::start(path, ip, options.port)?;
  232. let server_url = format!("http://{server_url}");
  233. dev_url = Some(server_url.parse().unwrap());
  234. if let Some(c) = &mut options.config {
  235. if let Some(build) = c
  236. .0
  237. .as_object_mut()
  238. .and_then(|root| root.get_mut("build"))
  239. .and_then(|build| build.as_object_mut())
  240. {
  241. build.insert("devUrl".into(), server_url.into());
  242. }
  243. } else {
  244. options
  245. .config
  246. .replace(crate::ConfigValue(serde_json::json!({
  247. "build": {
  248. "devUrl": server_url
  249. }
  250. })));
  251. }
  252. reload_config(options.config.as_ref().map(|c| &c.0))?;
  253. }
  254. }
  255. }
  256. if !options.no_dev_server_wait {
  257. if let Some(url) = dev_url {
  258. let host = url
  259. .host()
  260. .unwrap_or_else(|| panic!("No host name in the URL"));
  261. let port = url
  262. .port_or_known_default()
  263. .unwrap_or_else(|| panic!("No port number in the URL"));
  264. let addrs;
  265. let addr;
  266. let addrs = match host {
  267. url::Host::Domain(domain) => {
  268. use std::net::ToSocketAddrs;
  269. addrs = (domain, port).to_socket_addrs()?;
  270. addrs.as_slice()
  271. }
  272. url::Host::Ipv4(ip) => {
  273. addr = (ip, port).into();
  274. std::slice::from_ref(&addr)
  275. }
  276. url::Host::Ipv6(ip) => {
  277. addr = (ip, port).into();
  278. std::slice::from_ref(&addr)
  279. }
  280. };
  281. let mut i = 0;
  282. let sleep_interval = std::time::Duration::from_secs(2);
  283. let timeout_duration = std::time::Duration::from_secs(1);
  284. let max_attempts = 90;
  285. 'waiting: loop {
  286. for addr in addrs.iter() {
  287. if std::net::TcpStream::connect_timeout(addr, timeout_duration).is_ok() {
  288. break 'waiting;
  289. }
  290. }
  291. if i % 3 == 1 {
  292. log::warn!("Waiting for your frontend dev server to start on {url}...",);
  293. }
  294. i += 1;
  295. if i == max_attempts {
  296. 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());
  297. exit(1);
  298. }
  299. std::thread::sleep(sleep_interval);
  300. }
  301. }
  302. }
  303. Ok(())
  304. }
  305. pub fn wait_dev_process<
  306. C: DevProcess + Send + 'static,
  307. F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static,
  308. >(
  309. child: C,
  310. on_exit: F,
  311. ) {
  312. std::thread::spawn(move || {
  313. let code = child
  314. .wait()
  315. .ok()
  316. .and_then(|status| status.code())
  317. .or(Some(1));
  318. on_exit(
  319. code,
  320. if child.manually_killed_process() {
  321. ExitReason::TriggeredKill
  322. } else {
  323. ExitReason::NormalExit
  324. },
  325. );
  326. });
  327. }
  328. pub fn on_app_exit(code: Option<i32>, reason: ExitReason, exit_on_panic: bool, no_watch: bool) {
  329. if no_watch
  330. || (!matches!(reason, ExitReason::TriggeredKill)
  331. && (exit_on_panic || matches!(reason, ExitReason::NormalExit)))
  332. {
  333. kill_before_dev_process();
  334. exit(code.unwrap_or(0));
  335. }
  336. }
  337. pub fn kill_before_dev_process() {
  338. if let Some(child) = BEFORE_DEV.get() {
  339. let child = child.lock().unwrap();
  340. let kill_before_dev_flag = KILL_BEFORE_DEV_FLAG.get().unwrap();
  341. if kill_before_dev_flag.load(Ordering::Relaxed) {
  342. return;
  343. }
  344. kill_before_dev_flag.store(true, Ordering::Relaxed);
  345. #[cfg(windows)]
  346. {
  347. let powershell_path = std::env::var("SYSTEMROOT").map_or_else(
  348. |_| "powershell.exe".to_string(),
  349. |p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
  350. );
  351. let _ = Command::new(powershell_path)
  352. .arg("-NoProfile")
  353. .arg("-Command")
  354. .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()))
  355. .status();
  356. }
  357. #[cfg(unix)]
  358. {
  359. use std::io::Write;
  360. let mut kill_children_script_path = std::env::temp_dir();
  361. kill_children_script_path.push("tauri-stop-dev-processes.sh");
  362. if !kill_children_script_path.exists() {
  363. if let Ok(mut file) = std::fs::File::create(&kill_children_script_path) {
  364. use std::os::unix::fs::PermissionsExt;
  365. let _ = file.write_all(KILL_CHILDREN_SCRIPT);
  366. let mut permissions = file.metadata().unwrap().permissions();
  367. permissions.set_mode(0o770);
  368. let _ = file.set_permissions(permissions);
  369. }
  370. }
  371. let _ = Command::new(&kill_children_script_path)
  372. .arg(child.id().to_string())
  373. .output();
  374. }
  375. let _ = child.kill();
  376. }
  377. }