1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12mod cargo;
13mod node;
14mod util;
15use std::{
16 collections::{HashMap, HashSet},
17 fmt, io, mem,
18 path::PathBuf,
19 sync::Arc,
20 time::Duration,
21};
22
23pub use cargo::BuildError;
24use node::*;
25
26use zng_app::{
27 APP, AppExtension, DInstant, INSTANT,
28 event::{event, event_args},
29 handler::async_clmv,
30 update::UPDATES,
31};
32use zng_app_context::{LocalContext, app_local};
33use zng_ext_fs_watcher::WATCHER;
34pub use zng_ext_hot_reload_proc_macros::hot_node;
35use zng_task::{SignalOnce, parking_lot::Mutex};
36use zng_txt::Txt;
37use zng_unique_id::hot_reload::HOT_STATICS;
38use zng_unit::TimeUnits as _;
39use zng_var::{ResponseVar, Var};
40
41#[doc(inline)]
42pub use zng_unique_id::{hot_static, hot_static_ref, lazy_static};
43
44#[macro_export]
52macro_rules! zng_hot_entry {
53 () => {
54 #[doc(hidden)] pub use $crate::zng_hot_entry;
56
57 #[unsafe(no_mangle)] #[doc(hidden)] pub extern "C" fn zng_hot_entry(
60 manifest_dir: &&str,
61 node_name: &&'static str,
62 ctx: &mut $crate::zng_hot_entry::LocalContext,
63 exchange: &mut $crate::HotEntryExchange,
64 ) {
65 $crate::zng_hot_entry::entry(manifest_dir, node_name, ctx, exchange)
66 }
67
68 #[unsafe(no_mangle)] #[doc(hidden)]
70 pub extern "C" fn zng_hot_entry_init(patch: &$crate::StaticPatch) {
71 $crate::zng_hot_entry::init(patch)
72 }
73 };
74}
75
76#[doc(hidden)]
77pub mod zng_hot_entry {
78 pub use linkme as __linkme;
79
80 pub use crate::node::{HotNode, HotNodeArgs, HotNodeHost};
81 use crate::{HotEntryExchange, StaticPatch};
82 pub use zng_app_context::LocalContext;
83
84 pub struct HotNodeEntry {
85 pub manifest_dir: &'static str,
86 pub hot_node_name: &'static str,
87 pub hot_node_fn: fn(HotNodeArgs) -> HotNode,
88 }
89
90 #[linkme::distributed_slice]
91 pub static HOT_NODES: [HotNodeEntry];
92
93 pub fn entry(manifest_dir: &str, node_name: &'static str, ctx: &mut LocalContext, exchange: &mut HotEntryExchange) {
94 for entry in HOT_NODES.iter() {
95 if node_name == entry.hot_node_name && manifest_dir == entry.manifest_dir {
96 let args = match std::mem::replace(exchange, HotEntryExchange::Responding) {
97 HotEntryExchange::Request(args) => args,
98 _ => panic!("bad request"),
99 };
100 let node = ctx.with_context(|| (entry.hot_node_fn)(args));
101 *exchange = HotEntryExchange::Response(Some(node));
102 return;
103 }
104 }
105 *exchange = HotEntryExchange::Response(None);
106 }
107
108 pub fn init(statics: &StaticPatch) {
109 std::panic::set_hook(Box::new(|args| {
110 eprintln!("PANIC IN HOT LOADED LIBRARY, ABORTING");
111 crate::util::crash_handler(args);
112 zng_env::exit(101);
113 }));
114
115 unsafe { statics.apply() }
117 }
118}
119
120type StaticPatchersMap = HashMap<&'static dyn zng_unique_id::hot_reload::PatchKey, unsafe fn(*const ()) -> *const ()>;
121
122#[doc(hidden)]
123#[derive(Clone)]
124#[repr(C)]
125pub struct StaticPatch {
126 tracing: tracing_shared::SharedLogger,
127 entries: Arc<StaticPatchersMap>,
128}
129impl StaticPatch {
130 pub fn capture() -> Self {
132 let mut entries = StaticPatchersMap::with_capacity(HOT_STATICS.len());
133 for (key, val) in HOT_STATICS.iter() {
134 match entries.entry(*key) {
135 std::collections::hash_map::Entry::Vacant(e) => {
136 e.insert(*val);
137 }
138 std::collections::hash_map::Entry::Occupied(_) => {
139 panic!("repeated hot static key `{key:?}`");
140 }
141 }
142 }
143
144 Self {
145 entries: Arc::new(entries),
146 tracing: tracing_shared::SharedLogger::new(),
147 }
148 }
149
150 unsafe fn apply(&self) {
152 self.tracing.install();
153
154 for (key, patch) in HOT_STATICS.iter() {
155 if let Some(val) = self.entries.get(key) {
156 unsafe {
159 patch(val(std::ptr::null()));
160 }
161 } else {
162 eprintln!("did not find `{key:?}` to patch, static references may fail");
163 }
164 }
165 }
166}
167
168#[derive(Clone, PartialEq, Debug)]
170#[non_exhaustive]
171pub struct HotStatus {
172 pub manifest_dir: Txt,
176
177 pub building: Option<DInstant>,
179
180 pub last_build: Result<Duration, BuildError>,
184
185 pub rebuild_count: usize,
187}
188impl HotStatus {
189 pub fn ok(&self) -> Option<Duration> {
191 self.last_build.as_ref().ok().copied()
192 }
193
194 pub fn is_cancelled(&self) -> bool {
196 matches!(&self.last_build, Err(BuildError::Cancelled))
197 }
198
199 pub fn err(&self) -> Option<&BuildError> {
201 self.last_build.as_ref().err().filter(|e| !matches!(e, BuildError::Cancelled))
202 }
203}
204
205#[derive(Default)]
219pub struct HotReloadManager {
220 libs: HashMap<&'static str, WatchedLib>,
221 static_patch: Option<StaticPatch>,
222}
223impl AppExtension for HotReloadManager {
224 fn init(&mut self) {
225 let mut status = vec![];
227 for entry in crate::zng_hot_entry::HOT_NODES.iter() {
228 if let std::collections::hash_map::Entry::Vacant(e) = self.libs.entry(entry.manifest_dir) {
229 e.insert(WatchedLib::default());
230 WATCHER.watch_dir(entry.manifest_dir, true).perm();
231
232 status.push(HotStatus {
233 manifest_dir: entry.manifest_dir.into(),
234 building: None,
235 last_build: Ok(Duration::MAX),
236 rebuild_count: 0,
237 });
238 }
239 }
240 HOT_RELOAD_SV.read().status.set(status);
241 }
242
243 fn event_preview(&mut self, update: &mut zng_app::update::EventUpdate) {
244 if let Some(args) = zng_ext_fs_watcher::FS_CHANGES_EVENT.on(update) {
245 for (manifest_dir, watched) in self.libs.iter_mut() {
246 if args.changes_for_path(manifest_dir.as_ref()).next().is_some() {
247 watched.rebuild((*manifest_dir).into(), self.static_patch.get_or_insert_with(StaticPatch::capture));
248 }
249 }
250 }
251 }
252
253 fn update_preview(&mut self) {
254 for (manifest_dir, watched) in self.libs.iter_mut() {
255 if let Some(b) = &watched.building
256 && let Some(r) = b.rebuild_load.rsp()
257 {
258 let build_time = b.start_time.elapsed();
259 let mut lib = None;
260 let status_r = match r {
261 Ok(l) => {
262 lib = Some(l);
263 Ok(build_time)
264 }
265 Err(e) => {
266 if matches!(&e, BuildError::Cancelled) {
267 tracing::warn!("cancelled rebuild `{manifest_dir}`");
268 } else {
269 tracing::error!("failed rebuild `{manifest_dir}`, {e}");
270 }
271 Err(e)
272 }
273 };
274 if let Some(lib) = lib {
275 tracing::info!("rebuilt and reloaded `{manifest_dir}` in {build_time:?}");
276 HOT_RELOAD.set(lib.clone());
277 HOT_RELOAD_EVENT.notify(HotReloadArgs::now(lib));
278 }
279
280 watched.building = None;
281
282 let manifest_dir = *manifest_dir;
283 HOT_RELOAD_SV.read().status.modify(move |s| {
284 let s = s.iter_mut().find(|s| s.manifest_dir == manifest_dir).unwrap();
285 s.building = None;
286 s.last_build = status_r;
287 s.rebuild_count += 1;
288 });
289
290 if mem::take(&mut watched.rebuild_again) {
291 HOT_RELOAD_SV.write().rebuild_requests.push(manifest_dir.into());
292 }
293 }
294 }
295
296 let mut sv = HOT_RELOAD_SV.write();
297 let requests: HashSet<Txt> = sv.cancel_requests.drain(..).collect();
298 for r in requests {
299 if let Some(watched) = self.libs.get_mut(r.as_str())
300 && let Some(b) = &watched.building
301 {
302 b.cancel_build.set();
303 }
304 }
305
306 let requests: HashSet<Txt> = sv.rebuild_requests.drain(..).collect();
307 drop(sv);
308 for r in requests {
309 if let Some(watched) = self.libs.get_mut(r.as_str()) {
310 watched.rebuild(r, self.static_patch.get_or_insert_with(StaticPatch::capture));
311 } else {
312 tracing::error!("cannot rebuild `{r}`, unknown");
313 }
314 }
315 }
316}
317
318type RebuildVar = ResponseVar<Result<PathBuf, BuildError>>;
319
320type RebuildLoadVar = ResponseVar<Result<HotLib, BuildError>>;
321
322#[derive(Clone, Debug, PartialEq)]
328#[non_exhaustive]
329pub struct BuildArgs {
330 pub manifest_dir: Txt,
332 pub cancel_build: SignalOnce,
337}
338impl BuildArgs {
339 pub fn build(&self, package: Option<&str>) -> Option<RebuildVar> {
343 Some(cargo::build(
344 &self.manifest_dir,
345 "--package",
346 package.unwrap_or(""),
347 "",
348 "",
349 self.cancel_build.clone(),
350 ))
351 }
352
353 pub fn build_example(&self, package: Option<&str>, example: &str) -> Option<RebuildVar> {
358 Some(cargo::build(
359 &self.manifest_dir,
360 "--package",
361 package.unwrap_or(""),
362 "--example",
363 example,
364 self.cancel_build.clone(),
365 ))
366 }
367
368 pub fn build_bin(&self, package: Option<&str>, bin: &str) -> Option<RebuildVar> {
373 Some(cargo::build(
374 &self.manifest_dir,
375 "--package",
376 package.unwrap_or(""),
377 "--bin",
378 bin,
379 self.cancel_build.clone(),
380 ))
381 }
382
383 pub fn build_manifest(&self, path: &str) -> Option<RebuildVar> {
387 Some(cargo::build(
388 &self.manifest_dir,
389 "--manifest-path",
390 path,
391 "",
392 "",
393 self.cancel_build.clone(),
394 ))
395 }
396
397 pub fn custom(&self, cmd: std::process::Command) -> Option<RebuildVar> {
404 Some(cargo::build_custom(&self.manifest_dir, cmd, self.cancel_build.clone()))
405 }
406
407 pub fn custom_env(&self, mut var_key: &str) -> Option<RebuildVar> {
419 if var_key.is_empty() {
420 var_key = "ZNG_HOT_RELOAD_REBUILDER";
421 }
422
423 let custom = std::env::var(var_key).ok()?;
424 let mut custom = custom.split(' ');
425
426 let subcommand = custom.next()?;
427
428 let mut cmd = std::process::Command::new("cargo");
429 cmd.arg(subcommand);
430 cmd.args(custom);
431
432 self.custom(cmd)
433 }
434
435 pub fn default_build(&self) -> Option<RebuildVar> {
441 self.custom_env("").or_else(|| self.build(None))
442 }
443}
444
445#[expect(non_camel_case_types)]
451pub struct HOT_RELOAD;
452impl HOT_RELOAD {
453 pub fn status(&self) -> Var<Vec<HotStatus>> {
455 HOT_RELOAD_SV.read().status.read_only()
456 }
457
458 pub fn rebuilder(&self, rebuilder: impl FnMut(BuildArgs) -> Option<RebuildVar> + Send + 'static) {
469 HOT_RELOAD_SV.write().rebuilders.get_mut().push(Box::new(rebuilder));
470 }
471
472 pub fn rebuild(&self, manifest_dir: impl Into<Txt>) {
476 HOT_RELOAD_SV.write().rebuild_requests.push(manifest_dir.into());
477 UPDATES.update(None);
478 }
479
480 pub fn cancel(&self, manifest_dir: impl Into<Txt>) {
482 HOT_RELOAD_SV.write().cancel_requests.push(manifest_dir.into());
483 UPDATES.update(None);
484 }
485
486 pub(crate) fn lib(&self, manifest_dir: &'static str) -> Option<HotLib> {
487 HOT_RELOAD_SV
488 .read()
489 .libs
490 .iter()
491 .rev()
492 .find(|l| l.manifest_dir() == manifest_dir)
493 .cloned()
494 }
495
496 fn set(&self, lib: HotLib) {
497 HOT_RELOAD_SV.write().libs.push(lib);
500 }
501}
502app_local! {
503 static HOT_RELOAD_SV: HotReloadService = {
504 APP.extensions().require::<HotReloadManager>();
505 HotReloadService {
506 libs: vec![],
507 rebuilders: Mutex::new(vec![]),
508 status: zng_var::var(vec![]),
509 rebuild_requests: vec![],
510 cancel_requests: vec![],
511 }
512 };
513}
514struct HotReloadService {
515 libs: Vec<HotLib>,
516 #[expect(clippy::type_complexity)]
518 rebuilders: Mutex<Vec<Box<dyn FnMut(BuildArgs) -> Option<RebuildVar> + Send + 'static>>>,
519
520 status: Var<Vec<HotStatus>>,
521 rebuild_requests: Vec<Txt>,
522 cancel_requests: Vec<Txt>,
523}
524impl HotReloadService {
525 fn rebuild_reload(&mut self, manifest_dir: Txt, static_patch: &StaticPatch) -> (RebuildLoadVar, SignalOnce) {
526 let (rebuild, cancel) = self.rebuild(manifest_dir.clone());
527 let rebuild_load = zng_task::respond(async_clmv!(static_patch, {
528 let build_path = rebuild.wait_rsp().await?;
529
530 let file_name = match build_path.file_name() {
532 Some(f) => f.to_string_lossy(),
533 None => return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "dylib path does not have a file name").into()),
534 };
535
536 for p in glob::glob(&format!("{}/zng-hot-{file_name}-*", build_path.parent().unwrap().display()))
538 .unwrap()
539 .flatten()
540 {
541 let _ = std::fs::remove_file(p);
542 }
543
544 let mut unique_path = build_path.clone();
545 let ts = std::time::SystemTime::now()
546 .duration_since(std::time::UNIX_EPOCH)
547 .unwrap()
548 .as_millis();
549 unique_path.set_file_name(format!("zng-hot-{file_name}-{ts:x}"));
550 std::fs::copy(&build_path, &unique_path)?;
551
552 let dylib = zng_task::wait(move || HotLib::new(&static_patch, manifest_dir, unique_path));
553 match zng_task::with_deadline(dylib, 10.secs()).await {
554 Ok(r) => r.map_err(Into::into),
555 Err(_) => Err(BuildError::Io(Arc::new(io::Error::new(
556 io::ErrorKind::TimedOut,
557 "hot dylib did not init after 10s",
558 )))),
559 }
560 }));
561 (rebuild_load, cancel)
562 }
563
564 fn rebuild(&mut self, manifest_dir: Txt) -> (RebuildVar, SignalOnce) {
565 for r in self.rebuilders.get_mut() {
566 let cancel = SignalOnce::new();
567 let args = BuildArgs {
568 manifest_dir: manifest_dir.clone(),
569 cancel_build: cancel.clone(),
570 };
571 if let Some(r) = r(args.clone()) {
572 return (r, cancel);
573 }
574 }
575 let cancel = SignalOnce::new();
576 let args = BuildArgs {
577 manifest_dir: manifest_dir.clone(),
578 cancel_build: cancel.clone(),
579 };
580 (args.default_build().unwrap(), cancel)
581 }
582}
583
584event_args! {
585 pub struct HotReloadArgs {
587 pub(crate) lib: HotLib,
589
590 ..
591
592 fn delivery_list(&self, list: &mut UpdateDeliveryList) {
593 list.search_all();
594 }
595 }
596}
597impl HotReloadArgs {
598 pub fn manifest_dir(&self) -> &Txt {
600 self.lib.manifest_dir()
601 }
602}
603
604event! {
605 pub static HOT_RELOAD_EVENT: HotReloadArgs;
609}
610
611#[derive(Default)]
612struct WatchedLib {
613 building: Option<BuildingLib>,
614 rebuild_again: bool,
615}
616impl WatchedLib {
617 fn rebuild(&mut self, manifest_dir: Txt, static_path: &StaticPatch) {
618 if let Some(b) = &self.building {
619 if b.start_time.elapsed() > WATCHER.debounce().get() + 34.ms() {
620 b.cancel_build.set();
627 self.rebuild_again = true;
628 }
629 } else {
630 let start_time = INSTANT.now();
631 tracing::info!("rebuilding `{manifest_dir}`");
632
633 let mut sv = HOT_RELOAD_SV.write();
634
635 let (rebuild_load, cancel_build) = sv.rebuild_reload(manifest_dir.clone(), static_path);
636 self.building = Some(BuildingLib {
637 start_time,
638 rebuild_load,
639 cancel_build,
640 });
641
642 sv.status.modify(move |s| {
643 s.iter_mut().find(|s| s.manifest_dir == manifest_dir).unwrap().building = Some(start_time);
644 });
645 }
646 }
647}
648
649struct BuildingLib {
650 start_time: DInstant,
651 rebuild_load: RebuildLoadVar,
652 cancel_build: SignalOnce,
653}
654
655#[doc(hidden)]
656pub enum HotEntryExchange {
657 Request(HotNodeArgs),
658 Responding,
659 Response(Option<HotNode>),
660}
661
662#[derive(Clone)]
664pub(crate) struct HotLib {
665 manifest_dir: Txt,
666 lib: Arc<libloading::Library>,
667 hot_entry: unsafe extern "C" fn(&&str, &&'static str, &mut LocalContext, &mut HotEntryExchange),
668}
669impl PartialEq for HotLib {
670 fn eq(&self, other: &Self) -> bool {
671 Arc::ptr_eq(&self.lib, &other.lib)
672 }
673}
674impl fmt::Debug for HotLib {
675 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
676 f.debug_struct("HotLib")
677 .field("manifest_dir", &self.manifest_dir)
678 .finish_non_exhaustive()
679 }
680}
681impl HotLib {
682 pub fn new(patch: &StaticPatch, manifest_dir: Txt, lib: impl AsRef<std::ffi::OsStr>) -> Result<Self, libloading::Error> {
683 unsafe {
684 let lib = libloading::Library::new(lib.as_ref())?;
690
691 let init: unsafe extern "C" fn(&StaticPatch) = *lib.get(b"zng_hot_entry_init")?;
693 init(patch);
694
695 Ok(Self {
696 manifest_dir,
697 hot_entry: *lib.get(b"zng_hot_entry")?,
698 lib: Arc::new(lib),
699 })
700 }
701 }
702
703 pub fn manifest_dir(&self) -> &Txt {
705 &self.manifest_dir
706 }
707
708 pub fn instantiate(&self, hot_node_name: &'static str, ctx: &mut LocalContext, args: HotNodeArgs) -> Option<HotNode> {
709 let mut exchange = HotEntryExchange::Request(args);
710 unsafe { (self.hot_entry)(&self.manifest_dir.as_str(), &hot_node_name, ctx, &mut exchange) };
712 let mut node = match exchange {
713 HotEntryExchange::Response(n) => n,
714 _ => None,
715 };
716 if let Some(n) = &mut node {
717 n._lib = Some(self.lib.clone());
718 }
719 node
720 }
721}