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