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")))]
21#![warn(unused_extern_crates)]
22#![warn(missing_docs)]
23
24mod cargo;
25mod node;
26mod util;
27use std::{collections::HashMap, fmt, io, mem, path::PathBuf, sync::Arc, time::Duration};
28
29pub use cargo::BuildError;
30use node::*;
31
32use zng_app::{
33 DInstant, INSTANT,
34 event::{event, event_args},
35 handler::async_clmv,
36 update::UPDATES,
37};
38use zng_app_context::{LocalContext, app_local};
39use zng_ext_fs_watcher::WATCHER;
40pub use zng_ext_hot_reload_proc_macros::hot_node;
41use zng_task::{SignalOnce, parking_lot::Mutex};
42use zng_txt::Txt;
43use zng_unique_id::hot_reload::HOT_STATICS;
44use zng_unit::TimeUnits as _;
45use zng_var::{ResponseVar, Var};
46
47#[doc(inline)]
48pub use zng_unique_id::{hot_static, hot_static_ref, lazy_static};
49
50#[macro_export]
58macro_rules! zng_hot_entry {
59 () => {
60 #[doc(hidden)] pub use $crate::zng_hot_entry;
62
63 #[unsafe(no_mangle)] #[doc(hidden)] pub extern "C" fn zng_hot_entry(
66 manifest_dir: &&str,
67 node_name: &&'static str,
68 ctx: &mut $crate::zng_hot_entry::LocalContext,
69 exchange: &mut $crate::HotEntryExchange,
70 ) {
71 $crate::zng_hot_entry::entry(manifest_dir, node_name, ctx, exchange)
72 }
73
74 #[unsafe(no_mangle)] #[doc(hidden)]
76 pub extern "C" fn zng_hot_entry_init(patch: &$crate::StaticPatch) {
77 $crate::zng_hot_entry::init(patch)
78 }
79 };
80}
81
82#[doc(hidden)]
83pub mod zng_hot_entry {
84 pub use linkme as __linkme;
85
86 pub use crate::node::{HotNode, HotNodeArgs, HotNodeHost};
87 use crate::{HotEntryExchange, StaticPatch};
88 pub use zng_app_context::LocalContext;
89
90 pub struct HotNodeEntry {
91 pub manifest_dir: &'static str,
92 pub hot_node_name: &'static str,
93 pub hot_node_fn: fn(HotNodeArgs) -> HotNode,
94 }
95
96 #[linkme::distributed_slice]
97 pub static HOT_NODES: [HotNodeEntry];
98
99 pub fn entry(manifest_dir: &str, node_name: &'static str, ctx: &mut LocalContext, exchange: &mut HotEntryExchange) {
100 for entry in HOT_NODES.iter() {
101 if node_name == entry.hot_node_name && manifest_dir == entry.manifest_dir {
102 let args = match std::mem::replace(exchange, HotEntryExchange::Responding) {
103 HotEntryExchange::Request(args) => args,
104 _ => panic!("bad request"),
105 };
106 let node = ctx.with_context(|| (entry.hot_node_fn)(args));
107 *exchange = HotEntryExchange::Response(Some(node));
108 return;
109 }
110 }
111 *exchange = HotEntryExchange::Response(None);
112 }
113
114 pub fn init(statics: &StaticPatch) {
115 std::panic::set_hook(Box::new(|args| {
116 eprintln!("PANIC IN HOT LOADED LIBRARY, ABORTING");
117 crate::util::crash_handler(args);
118 zng_env::exit(101);
119 }));
120
121 unsafe { statics.apply() }
123 }
124}
125
126type StaticPatchersMap = HashMap<&'static dyn zng_unique_id::hot_reload::PatchKey, unsafe fn(*const ()) -> *const ()>;
127
128#[doc(hidden)]
129#[derive(Clone)]
130#[repr(C)]
131pub struct StaticPatch {
132 tracing: tracing_shared::SharedLogger,
133 entries: Arc<StaticPatchersMap>,
134}
135impl StaticPatch {
136 pub fn capture() -> Self {
138 let mut entries = StaticPatchersMap::with_capacity(HOT_STATICS.len());
139 for (key, val) in HOT_STATICS.iter() {
140 match entries.entry(*key) {
141 std::collections::hash_map::Entry::Vacant(e) => {
142 e.insert(*val);
143 }
144 std::collections::hash_map::Entry::Occupied(_) => {
145 panic!("repeated hot static key `{key:?}`");
146 }
147 }
148 }
149
150 Self {
151 entries: Arc::new(entries),
152 tracing: tracing_shared::SharedLogger::new(),
153 }
154 }
155
156 unsafe fn apply(&self) {
158 self.tracing.install();
159
160 for (key, patch) in HOT_STATICS.iter() {
161 if let Some(val) = self.entries.get(key) {
162 unsafe {
165 patch(val(std::ptr::null()));
166 }
167 } else {
168 eprintln!("did not find `{key:?}` to patch, static references may fail");
169 }
170 }
171 }
172}
173
174#[derive(Clone, PartialEq, Debug)]
176#[non_exhaustive]
177pub struct HotStatus {
178 pub manifest_dir: Txt,
182
183 pub building: Option<DInstant>,
185
186 pub last_build: Result<Duration, BuildError>,
190
191 pub rebuild_count: usize,
193}
194impl HotStatus {
195 pub fn ok(&self) -> Option<Duration> {
197 self.last_build.as_ref().ok().copied()
198 }
199
200 pub fn is_cancelled(&self) -> bool {
202 matches!(&self.last_build, Err(BuildError::Cancelled))
203 }
204
205 pub fn err(&self) -> Option<&BuildError> {
207 self.last_build.as_ref().err().filter(|e| !matches!(e, BuildError::Cancelled))
208 }
209}
210
211type RebuildVar = ResponseVar<Result<PathBuf, BuildError>>;
212
213type RebuildLoadVar = ResponseVar<Result<HotLib, BuildError>>;
214
215#[derive(Clone, Debug, PartialEq)]
221#[non_exhaustive]
222pub struct BuildArgs {
223 pub manifest_dir: Txt,
225 pub cancel_build: SignalOnce,
230}
231impl BuildArgs {
232 pub fn build(&self, package: Option<&str>) -> Option<RebuildVar> {
236 Some(cargo::build(
237 &self.manifest_dir,
238 "--package",
239 package.unwrap_or(""),
240 "",
241 "",
242 self.cancel_build.clone(),
243 ))
244 }
245
246 pub fn build_example(&self, package: Option<&str>, example: &str) -> Option<RebuildVar> {
251 Some(cargo::build(
252 &self.manifest_dir,
253 "--package",
254 package.unwrap_or(""),
255 "--example",
256 example,
257 self.cancel_build.clone(),
258 ))
259 }
260
261 pub fn build_bin(&self, package: Option<&str>, bin: &str) -> Option<RebuildVar> {
266 Some(cargo::build(
267 &self.manifest_dir,
268 "--package",
269 package.unwrap_or(""),
270 "--bin",
271 bin,
272 self.cancel_build.clone(),
273 ))
274 }
275
276 pub fn build_manifest(&self, path: &str) -> Option<RebuildVar> {
280 Some(cargo::build(
281 &self.manifest_dir,
282 "--manifest-path",
283 path,
284 "",
285 "",
286 self.cancel_build.clone(),
287 ))
288 }
289
290 pub fn custom(&self, cmd: std::process::Command) -> Option<RebuildVar> {
297 Some(cargo::build_custom(&self.manifest_dir, cmd, self.cancel_build.clone()))
298 }
299
300 pub fn custom_env(&self, mut var_key: &str) -> Option<RebuildVar> {
312 if var_key.is_empty() {
313 var_key = "ZNG_HOT_RELOAD_REBUILDER";
314 }
315
316 let custom = std::env::var(var_key).ok()?;
317 let mut custom = custom.split(' ');
318
319 let subcommand = custom.next()?;
320
321 let mut cmd = std::process::Command::new("cargo");
322 cmd.arg(subcommand);
323 cmd.args(custom);
324
325 self.custom(cmd)
326 }
327
328 pub fn default_build(&self) -> Option<RebuildVar> {
334 self.custom_env("").or_else(|| self.build(None))
335 }
336}
337
338#[expect(non_camel_case_types)]
340pub struct HOT_RELOAD;
341impl HOT_RELOAD {
342 pub fn status(&self) -> Var<Vec<HotStatus>> {
344 HOT_RELOAD_SV.read().status.read_only()
345 }
346
347 pub fn rebuilder(&self, rebuilder: impl FnMut(BuildArgs) -> Option<RebuildVar> + Send + 'static) {
356 let rebuilder = Box::new(rebuilder);
357 UPDATES.once_update("HOT_RELOAD.rebuilder", move || {
358 HOT_RELOAD_SV.write().rebuilders.get_mut().push(rebuilder);
359 });
360 }
361
362 pub fn rebuild(&self, manifest_dir: impl Into<Txt>) {
366 let manifest_dir = manifest_dir.into();
367 UPDATES.once_update("HOT_RELOAD.rebuild", move || {
368 let mut sv = HOT_RELOAD_SV.write();
369 let s = &mut *sv;
370 if let Some((key, mut watched)) = s.watched_libs.remove_entry(manifest_dir.as_str()) {
371 let patch = s.static_patch.get_or_insert_with(StaticPatch::capture).clone();
372 drop(sv);
373 watched.rebuild(manifest_dir, patch);
374 HOT_RELOAD_SV.write().watched_libs.insert(key, watched);
375 } else {
376 tracing::error!("cannot rebuild `{manifest_dir}`, unknown");
377 }
378 });
379 }
380
381 pub fn cancel(&self, manifest_dir: impl Into<Txt>) {
383 let manifest_dir = manifest_dir.into();
384 UPDATES.once_update("HOT_RELOAD.cancel", move || {
385 let mut s = HOT_RELOAD_SV.write();
386 if let Some(watched) = s.watched_libs.get_mut(manifest_dir.as_str())
387 && let Some(b) = &watched.building
388 {
389 b.cancel_build.set();
390 }
391 });
392 }
393
394 pub(crate) fn lib(&self, manifest_dir: &'static str) -> Option<HotLib> {
395 HOT_RELOAD_SV
396 .read()
397 .libs
398 .iter()
399 .rev()
400 .find(|l| l.manifest_dir() == manifest_dir)
401 .cloned()
402 }
403}
404app_local! {
405 static HOT_RELOAD_SV: HotReloadService = {
406 let mut s = HotReloadService {
407 watched_libs: HashMap::default(),
408 static_patch: None,
409 libs: vec![],
410 rebuilders: Mutex::new(vec![]),
411 status: zng_var::var(vec![]),
412 };
413 s.init();
414 s
415 };
416}
417struct HotReloadService {
418 watched_libs: HashMap<&'static str, WatchedLib>,
419 static_patch: Option<StaticPatch>,
420
421 libs: Vec<HotLib>,
422 #[expect(clippy::type_complexity)]
424 rebuilders: Mutex<Vec<Box<dyn FnMut(BuildArgs) -> Option<RebuildVar> + Send + 'static>>>,
425
426 status: Var<Vec<HotStatus>>,
427}
428impl HotReloadService {
429 fn init(&mut self) {
430 let mut status = vec![];
432 for entry in crate::zng_hot_entry::HOT_NODES.iter() {
433 if let std::collections::hash_map::Entry::Vacant(e) = self.watched_libs.entry(entry.manifest_dir) {
434 e.insert(WatchedLib::default());
435 WATCHER.watch_dir(entry.manifest_dir, true).perm();
436
437 status.push(HotStatus {
438 manifest_dir: entry.manifest_dir.into(),
439 building: None,
440 last_build: Ok(Duration::MAX),
441 rebuild_count: 0,
442 });
443 }
444 }
445 self.status.set(status);
446
447 zng_ext_fs_watcher::FS_CHANGES_EVENT
448 .hook(|args| {
449 let mut watched_libs = mem::take(&mut HOT_RELOAD_SV.write().watched_libs);
450 let mut static_patch = None;
451 for (manifest_dir, watched) in watched_libs.iter_mut() {
452 if args.changes_for_path(manifest_dir.as_ref()).next().is_some() {
453 let patch = static_patch
454 .get_or_insert_with(|| HOT_RELOAD_SV.write().static_patch.get_or_insert_with(StaticPatch::capture).clone())
455 .clone();
456 watched.rebuild((*manifest_dir).into(), patch);
457 }
458 }
459 HOT_RELOAD_SV.write().watched_libs = watched_libs;
460 true
461 })
462 .perm();
463 }
464
465 fn on_rebuild(&mut self) {
466 for (manifest_dir, watched) in self.watched_libs.iter_mut() {
467 if let Some(b) = &watched.building
468 && let Some(r) = b.rebuild_load.rsp()
469 {
470 let build_time = b.start_time.elapsed();
471 let mut lib = None;
472 let status_r = match r {
473 Ok(l) => {
474 lib = Some(l);
475 Ok(build_time)
476 }
477 Err(e) => {
478 if matches!(&e, BuildError::Cancelled) {
479 tracing::warn!("cancelled rebuild `{manifest_dir}`");
480 } else {
481 tracing::error!("failed rebuild `{manifest_dir}`, {e}");
482 }
483 Err(e)
484 }
485 };
486 if let Some(lib) = lib {
487 tracing::info!("rebuilt and reloaded `{manifest_dir}` in {build_time:?}");
488 self.libs.push(lib.clone());
489 HOT_RELOAD_EVENT.notify(HotReloadArgs::now(lib));
490 }
491
492 watched.building = None;
493
494 let manifest_dir = *manifest_dir;
495 self.status.modify(move |s| {
496 let s = s.iter_mut().find(|s| s.manifest_dir == manifest_dir).unwrap();
497 s.building = None;
498 s.last_build = status_r;
499 s.rebuild_count += 1;
500 });
501
502 if mem::take(&mut watched.rebuild_again) {
503 HOT_RELOAD.rebuild(manifest_dir);
504 }
505 }
506 }
507 }
508
509 fn rebuild_reload(&mut self, manifest_dir: Txt, static_patch: &StaticPatch) -> (RebuildLoadVar, SignalOnce) {
510 let (rebuild, cancel) = self.rebuild(manifest_dir.clone());
511 let rebuild_load = zng_task::respond(async_clmv!(static_patch, {
512 let build_path = rebuild.wait_rsp().await?;
513
514 let file_name = match build_path.file_name() {
516 Some(f) => f.to_string_lossy(),
517 None => return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "dylib path does not have a file name").into()),
518 };
519
520 for p in glob::glob(&format!("{}/zng-hot-{file_name}-*", build_path.parent().unwrap().display()))
522 .unwrap()
523 .flatten()
524 {
525 let _ = std::fs::remove_file(p);
526 }
527
528 let mut unique_path = build_path.clone();
529 let ts = std::time::SystemTime::now()
530 .duration_since(std::time::UNIX_EPOCH)
531 .unwrap()
532 .as_millis();
533 unique_path.set_file_name(format!("zng-hot-{file_name}-{ts:x}"));
534 std::fs::copy(&build_path, &unique_path)?;
535
536 let dylib = zng_task::wait(move || HotLib::new(&static_patch, manifest_dir, unique_path));
537 match zng_task::with_deadline(dylib, 10.secs()).await {
538 Ok(r) => r.map_err(Into::into),
539 Err(_) => Err(BuildError::Io(Arc::new(io::Error::new(
540 io::ErrorKind::TimedOut,
541 "hot dylib did not init after 10s",
542 )))),
543 }
544 }));
545 (rebuild_load, cancel)
546 }
547
548 fn rebuild(&mut self, manifest_dir: Txt) -> (RebuildVar, SignalOnce) {
549 for r in self.rebuilders.get_mut() {
550 let cancel = SignalOnce::new();
551 let args = BuildArgs {
552 manifest_dir: manifest_dir.clone(),
553 cancel_build: cancel.clone(),
554 };
555 if let Some(r) = r(args.clone()) {
556 return (r, cancel);
557 }
558 }
559 let cancel = SignalOnce::new();
560 let args = BuildArgs {
561 manifest_dir: manifest_dir.clone(),
562 cancel_build: cancel.clone(),
563 };
564 (args.default_build().unwrap(), cancel)
565 }
566}
567
568event_args! {
569 pub struct HotReloadArgs {
571 pub(crate) lib: HotLib,
573
574 ..
575
576 fn is_in_target(&self, _id: WidgetId) -> bool {
577 true
578 }
579 }
580}
581impl HotReloadArgs {
582 pub fn manifest_dir(&self) -> &Txt {
584 self.lib.manifest_dir()
585 }
586}
587
588event! {
589 pub static HOT_RELOAD_EVENT: HotReloadArgs {
593 let _init = HOT_RELOAD_SV.write();
594 };
595}
596
597#[derive(Default)]
598struct WatchedLib {
599 building: Option<BuildingLib>,
600 rebuild_again: bool,
601}
602impl WatchedLib {
603 fn rebuild(&mut self, manifest_dir: Txt, static_patch: StaticPatch) {
604 if let Some(b) = &self.building {
605 if b.start_time.elapsed() > WATCHER.debounce().get() + 34.ms() {
606 b.cancel_build.set();
613 self.rebuild_again = true;
614 }
615 } else {
616 let start_time = INSTANT.now();
617 tracing::info!("rebuilding `{manifest_dir}`");
618
619 let mut sv = HOT_RELOAD_SV.write();
620 let (rebuild_load, cancel_build) = sv.rebuild_reload(manifest_dir.clone(), &static_patch);
621 rebuild_load
622 .hook(|a| {
623 if !a.value().is_done() {
624 return true;
625 }
626 HOT_RELOAD_SV.write().on_rebuild();
627 false
628 })
629 .perm();
630
631 self.building = Some(BuildingLib {
632 start_time,
633 rebuild_load,
634 cancel_build,
635 });
636
637 sv.status.modify(move |s| {
638 s.iter_mut().find(|s| s.manifest_dir == manifest_dir).unwrap().building = Some(start_time);
639 });
640 }
641 }
642}
643
644struct BuildingLib {
645 start_time: DInstant,
646 rebuild_load: RebuildLoadVar,
647 cancel_build: SignalOnce,
648}
649
650#[doc(hidden)]
651pub enum HotEntryExchange {
652 Request(HotNodeArgs),
653 Responding,
654 Response(Option<HotNode>),
655}
656
657#[derive(Clone)]
659pub(crate) struct HotLib {
660 manifest_dir: Txt,
661 lib: Arc<libloading::Library>,
662 hot_entry: unsafe extern "C" fn(&&str, &&'static str, &mut LocalContext, &mut HotEntryExchange),
663}
664impl PartialEq for HotLib {
665 fn eq(&self, other: &Self) -> bool {
666 Arc::ptr_eq(&self.lib, &other.lib)
667 }
668}
669impl fmt::Debug for HotLib {
670 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
671 f.debug_struct("HotLib")
672 .field("manifest_dir", &self.manifest_dir)
673 .finish_non_exhaustive()
674 }
675}
676impl HotLib {
677 pub fn new(patch: &StaticPatch, manifest_dir: Txt, lib: impl AsRef<std::ffi::OsStr>) -> Result<Self, libloading::Error> {
678 unsafe {
679 let lib = libloading::Library::new(lib.as_ref())?;
685
686 let init: unsafe extern "C" fn(&StaticPatch) = *lib.get(b"zng_hot_entry_init")?;
688 init(patch);
689
690 Ok(Self {
691 manifest_dir,
692 hot_entry: *lib.get(b"zng_hot_entry")?,
693 lib: Arc::new(lib),
694 })
695 }
696 }
697
698 pub fn manifest_dir(&self) -> &Txt {
700 &self.manifest_dir
701 }
702
703 pub fn instantiate(&self, hot_node_name: &'static str, ctx: &mut LocalContext, args: HotNodeArgs) -> Option<HotNode> {
704 let mut exchange = HotEntryExchange::Request(args);
705 unsafe { (self.hot_entry)(&self.manifest_dir.as_str(), &hot_node_name, ctx, &mut exchange) };
707 let mut node = match exchange {
708 HotEntryExchange::Response(n) => n,
709 _ => None,
710 };
711 if let Some(n) = &mut node {
712 n._lib = Some(self.lib.clone());
713 }
714 node
715 }
716}