1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/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 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::{ArcVar, ReadOnlyArcVar, ResponseVar, Var as _};
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 crate::node::{HotNode, HotNodeArgs, HotNodeHost};
79 use crate::{HotEntryExchange, StaticPatch};
80 pub use zng_app_context::LocalContext;
81
82 pub struct HotNodeEntry {
83 pub manifest_dir: &'static str,
84 pub hot_node_name: &'static str,
85 pub hot_node_fn: fn(HotNodeArgs) -> HotNode,
86 }
87
88 #[linkme::distributed_slice]
89 pub static HOT_NODES: [HotNodeEntry];
90
91 pub fn entry(manifest_dir: &str, node_name: &'static str, ctx: &mut LocalContext, exchange: &mut HotEntryExchange) {
92 for entry in HOT_NODES.iter() {
93 if node_name == entry.hot_node_name && manifest_dir == entry.manifest_dir {
94 let args = match std::mem::replace(exchange, HotEntryExchange::Responding) {
95 HotEntryExchange::Request(args) => args,
96 _ => panic!("bad request"),
97 };
98 let node = ctx.with_context(|| (entry.hot_node_fn)(args));
99 *exchange = HotEntryExchange::Response(Some(node));
100 return;
101 }
102 }
103 *exchange = HotEntryExchange::Response(None);
104 }
105
106 pub fn init(statics: &StaticPatch) {
107 std::panic::set_hook(Box::new(|args| {
108 eprintln!("PANIC IN HOT LOADED LIBRARY, ABORTING");
109 crate::util::crash_handler(args);
110 zng_env::exit(101);
111 }));
112
113 unsafe { statics.apply() }
115 }
116}
117
118type StaticPatchersMap = HashMap<&'static dyn zng_unique_id::hot_reload::PatchKey, unsafe fn(*const ()) -> *const ()>;
119
120#[doc(hidden)]
121#[derive(Clone)]
122#[repr(C)]
123pub struct StaticPatch {
124 tracing: tracing_shared::SharedLogger,
125 entries: Arc<StaticPatchersMap>,
126}
127impl StaticPatch {
128 pub fn capture() -> Self {
130 let mut entries = StaticPatchersMap::with_capacity(HOT_STATICS.len());
131 for (key, val) in HOT_STATICS.iter() {
132 match entries.entry(*key) {
133 std::collections::hash_map::Entry::Vacant(e) => {
134 e.insert(*val);
135 }
136 std::collections::hash_map::Entry::Occupied(_) => {
137 panic!("repeated hot static key `{key:?}`");
138 }
139 }
140 }
141
142 Self {
143 entries: Arc::new(entries),
144 tracing: tracing_shared::SharedLogger::new(),
145 }
146 }
147
148 unsafe fn apply(&self) {
150 self.tracing.install();
151
152 for (key, patch) in HOT_STATICS.iter() {
153 if let Some(val) = self.entries.get(key) {
154 unsafe {
157 patch(val(std::ptr::null()));
158 }
159 } else {
160 eprintln!("did not find `{key:?}` to patch, static references may fail");
161 }
162 }
163 }
164}
165
166#[derive(Clone, PartialEq, Debug)]
168pub struct HotStatus {
169 pub manifest_dir: Txt,
173
174 pub building: Option<DInstant>,
176
177 pub last_build: Result<Duration, BuildError>,
181
182 pub rebuild_count: usize,
184}
185impl HotStatus {
186 pub fn ok(&self) -> Option<Duration> {
188 self.last_build.as_ref().ok().copied()
189 }
190
191 pub fn is_cancelled(&self) -> bool {
193 matches!(&self.last_build, Err(BuildError::Cancelled))
194 }
195
196 pub fn err(&self) -> Option<&BuildError> {
198 self.last_build.as_ref().err().filter(|e| !matches!(e, BuildError::Cancelled))
199 }
200}
201
202#[derive(Default)]
216pub struct HotReloadManager {
217 libs: HashMap<&'static str, WatchedLib>,
218 static_patch: Option<StaticPatch>,
219}
220impl AppExtension for HotReloadManager {
221 fn init(&mut self) {
222 let mut status = vec![];
224 for entry in crate::zng_hot_entry::HOT_NODES.iter() {
225 if let std::collections::hash_map::Entry::Vacant(e) = self.libs.entry(entry.manifest_dir) {
226 e.insert(WatchedLib::default());
227 WATCHER.watch_dir(entry.manifest_dir, true).perm();
228
229 status.push(HotStatus {
230 manifest_dir: entry.manifest_dir.into(),
231 building: None,
232 last_build: Ok(Duration::MAX),
233 rebuild_count: 0,
234 });
235 }
236 }
237 HOT_RELOAD_SV.read().status.set(status);
238 }
239
240 fn event_preview(&mut self, update: &mut zng_app::update::EventUpdate) {
241 if let Some(args) = zng_ext_fs_watcher::FS_CHANGES_EVENT.on(update) {
242 for (manifest_dir, watched) in self.libs.iter_mut() {
243 if args.changes_for_path(manifest_dir.as_ref()).next().is_some() {
244 watched.rebuild((*manifest_dir).into(), self.static_patch.get_or_insert_with(StaticPatch::capture));
245 }
246 }
247 }
248 }
249
250 fn update_preview(&mut self) {
251 for (manifest_dir, watched) in self.libs.iter_mut() {
252 if let Some(b) = &watched.building {
253 if let Some(r) = b.rebuild_load.rsp() {
254 let build_time = b.start_time.elapsed();
255 let mut lib = None;
256 let status_r = match r {
257 Ok(l) => {
258 lib = Some(l);
259 Ok(build_time)
260 }
261 Err(e) => {
262 if matches!(&e, BuildError::Cancelled) {
263 tracing::warn!("cancelled rebuild `{manifest_dir}`");
264 } else {
265 tracing::error!("failed rebuild `{manifest_dir}`, {e}");
266 }
267 Err(e)
268 }
269 };
270 if let Some(lib) = lib {
271 tracing::info!("rebuilt and reloaded `{manifest_dir}` in {build_time:?}");
272 HOT_RELOAD.set(lib.clone());
273 HOT_RELOAD_EVENT.notify(HotReloadArgs::now(lib));
274 }
275
276 watched.building = None;
277
278 let manifest_dir = *manifest_dir;
279 HOT_RELOAD_SV.read().status.modify(move |s| {
280 let s = s.to_mut().iter_mut().find(|s| s.manifest_dir == manifest_dir).unwrap();
281 s.building = None;
282 s.last_build = status_r;
283 s.rebuild_count += 1;
284 });
285
286 if mem::take(&mut watched.rebuild_again) {
287 HOT_RELOAD_SV.write().rebuild_requests.push(manifest_dir.into());
288 }
289 }
290 }
291 }
292
293 let mut sv = HOT_RELOAD_SV.write();
294 let requests: HashSet<Txt> = sv.cancel_requests.drain(..).collect();
295 for r in requests {
296 if let Some(watched) = self.libs.get_mut(r.as_str()) {
297 if let Some(b) = &watched.building {
298 b.cancel_build.set();
299 }
300 }
301 }
302
303 let requests: HashSet<Txt> = sv.rebuild_requests.drain(..).collect();
304 drop(sv);
305 for r in requests {
306 if let Some(watched) = self.libs.get_mut(r.as_str()) {
307 watched.rebuild(r, self.static_patch.get_or_insert_with(StaticPatch::capture));
308 } else {
309 tracing::error!("cannot rebuild `{r}`, unknown");
310 }
311 }
312 }
313}
314
315type RebuildVar = ResponseVar<Result<PathBuf, BuildError>>;
316
317type RebuildLoadVar = ResponseVar<Result<HotLib, BuildError>>;
318
319#[derive(Clone, Debug, PartialEq)]
325pub struct BuildArgs {
326 pub manifest_dir: Txt,
328 pub cancel_build: SignalOnce,
333}
334impl BuildArgs {
335 pub fn build(&self, package: Option<&str>) -> Option<RebuildVar> {
339 Some(cargo::build(
340 &self.manifest_dir,
341 "--package",
342 package.unwrap_or(""),
343 "",
344 "",
345 self.cancel_build.clone(),
346 ))
347 }
348
349 pub fn build_example(&self, package: Option<&str>, example: &str) -> Option<RebuildVar> {
354 Some(cargo::build(
355 &self.manifest_dir,
356 "--package",
357 package.unwrap_or(""),
358 "--example",
359 example,
360 self.cancel_build.clone(),
361 ))
362 }
363
364 pub fn build_bin(&self, package: Option<&str>, bin: &str) -> Option<RebuildVar> {
369 Some(cargo::build(
370 &self.manifest_dir,
371 "--package",
372 package.unwrap_or(""),
373 "--bin",
374 bin,
375 self.cancel_build.clone(),
376 ))
377 }
378
379 pub fn build_manifest(&self, path: &str) -> Option<RebuildVar> {
383 Some(cargo::build(
384 &self.manifest_dir,
385 "--manifest-path",
386 path,
387 "",
388 "",
389 self.cancel_build.clone(),
390 ))
391 }
392
393 pub fn custom(&self, cmd: std::process::Command) -> Option<RebuildVar> {
400 Some(cargo::build_custom(&self.manifest_dir, cmd, self.cancel_build.clone()))
401 }
402
403 pub fn custom_env(&self, mut var_key: &str) -> Option<RebuildVar> {
415 if var_key.is_empty() {
416 var_key = "ZNG_HOT_RELOAD_REBUILDER";
417 }
418
419 let custom = std::env::var(var_key).ok()?;
420 let mut custom = custom.split(' ');
421
422 let subcommand = custom.next()?;
423
424 let mut cmd = std::process::Command::new("cargo");
425 cmd.arg(subcommand);
426 cmd.args(custom);
427
428 self.custom(cmd)
429 }
430
431 pub fn default_build(&self) -> Option<RebuildVar> {
437 self.custom_env("").or_else(|| self.build(None))
438 }
439}
440
441#[expect(non_camel_case_types)]
443pub struct HOT_RELOAD;
444impl HOT_RELOAD {
445 pub fn status(&self) -> ReadOnlyArcVar<Vec<HotStatus>> {
447 HOT_RELOAD_SV.read().status.read_only()
448 }
449
450 pub fn rebuilder(&self, rebuilder: impl FnMut(BuildArgs) -> Option<RebuildVar> + Send + 'static) {
461 HOT_RELOAD_SV.write().rebuilders.get_mut().push(Box::new(rebuilder));
462 }
463
464 pub fn rebuild(&self, manifest_dir: impl Into<Txt>) {
468 HOT_RELOAD_SV.write().rebuild_requests.push(manifest_dir.into());
469 UPDATES.update(None);
470 }
471
472 pub fn cancel(&self, manifest_dir: impl Into<Txt>) {
474 HOT_RELOAD_SV.write().cancel_requests.push(manifest_dir.into());
475 UPDATES.update(None);
476 }
477
478 pub(crate) fn lib(&self, manifest_dir: &'static str) -> Option<HotLib> {
479 HOT_RELOAD_SV
480 .read()
481 .libs
482 .iter()
483 .rev()
484 .find(|l| l.manifest_dir() == manifest_dir)
485 .cloned()
486 }
487
488 fn set(&self, lib: HotLib) {
489 HOT_RELOAD_SV.write().libs.push(lib);
492 }
493}
494app_local! {
495 static HOT_RELOAD_SV: HotReloadService = {
496 HotReloadService {
497 libs: vec![],
498 rebuilders: Mutex::new(vec![]),
499 status: zng_var::var(vec![]),
500 rebuild_requests: vec![],
501 cancel_requests: vec![],
502 }
503 };
504}
505struct HotReloadService {
506 libs: Vec<HotLib>,
507 #[expect(clippy::type_complexity)]
509 rebuilders: Mutex<Vec<Box<dyn FnMut(BuildArgs) -> Option<RebuildVar> + Send + 'static>>>,
510
511 status: ArcVar<Vec<HotStatus>>,
512 rebuild_requests: Vec<Txt>,
513 cancel_requests: Vec<Txt>,
514}
515impl HotReloadService {
516 fn rebuild_reload(&mut self, manifest_dir: Txt, static_patch: &StaticPatch) -> (RebuildLoadVar, SignalOnce) {
517 let (rebuild, cancel) = self.rebuild(manifest_dir.clone());
518 let rebuild_load = zng_task::respond(async_clmv!(static_patch, {
519 let build_path = rebuild.wait_into_rsp().await?;
520
521 let file_name = match build_path.file_name() {
523 Some(f) => f.to_string_lossy(),
524 None => return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "dylib path does not have a file name").into()),
525 };
526
527 for p in glob::glob(&format!("{}/zng-hot-{file_name}-*", build_path.parent().unwrap().display()))
529 .unwrap()
530 .flatten()
531 {
532 let _ = std::fs::remove_file(p);
533 }
534
535 let mut unique_path = build_path.clone();
536 let ts = std::time::SystemTime::now()
537 .duration_since(std::time::UNIX_EPOCH)
538 .unwrap()
539 .as_millis();
540 unique_path.set_file_name(format!("zng-hot-{file_name}-{ts:x}"));
541 std::fs::copy(&build_path, &unique_path)?;
542
543 let dylib = zng_task::wait(move || HotLib::new(&static_patch, manifest_dir, unique_path));
544 match zng_task::with_deadline(dylib, 2.secs()).await {
545 Ok(r) => r.map_err(Into::into),
546 Err(_) => Err(BuildError::Io(Arc::new(io::Error::new(
547 io::ErrorKind::TimedOut,
548 "hot dylib did not init after 2s",
549 )))),
550 }
551 }));
552 (rebuild_load, cancel)
553 }
554
555 fn rebuild(&mut self, manifest_dir: Txt) -> (RebuildVar, SignalOnce) {
556 for r in self.rebuilders.get_mut() {
557 let cancel = SignalOnce::new();
558 let args = BuildArgs {
559 manifest_dir: manifest_dir.clone(),
560 cancel_build: cancel.clone(),
561 };
562 if let Some(r) = r(args.clone()) {
563 return (r, cancel);
564 }
565 }
566 let cancel = SignalOnce::new();
567 let args = BuildArgs {
568 manifest_dir: manifest_dir.clone(),
569 cancel_build: cancel.clone(),
570 };
571 (args.default_build().unwrap(), cancel)
572 }
573}
574
575event_args! {
576 pub struct HotReloadArgs {
578 pub(crate) lib: HotLib,
580
581 ..
582
583 fn delivery_list(&self, list: &mut UpdateDeliveryList) {
584 list.search_all();
585 }
586 }
587}
588impl HotReloadArgs {
589 pub fn manifest_dir(&self) -> &Txt {
591 self.lib.manifest_dir()
592 }
593}
594
595event! {
596 pub static HOT_RELOAD_EVENT: HotReloadArgs;
600}
601
602#[derive(Default)]
603struct WatchedLib {
604 building: Option<BuildingLib>,
605 rebuild_again: bool,
606}
607impl WatchedLib {
608 fn rebuild(&mut self, manifest_dir: Txt, static_path: &StaticPatch) {
609 if let Some(b) = &self.building {
610 if b.start_time.elapsed() > WATCHER.debounce().get() + 34.ms() {
611 b.cancel_build.set();
618 self.rebuild_again = true;
619 }
620 } else {
621 let start_time = INSTANT.now();
622 tracing::info!("rebuilding `{manifest_dir}`");
623
624 let mut sv = HOT_RELOAD_SV.write();
625
626 let (rebuild_load, cancel_build) = sv.rebuild_reload(manifest_dir.clone(), static_path);
627 self.building = Some(BuildingLib {
628 start_time,
629 rebuild_load,
630 cancel_build,
631 });
632
633 sv.status.modify(move |s| {
634 s.to_mut().iter_mut().find(|s| s.manifest_dir == manifest_dir).unwrap().building = Some(start_time);
635 });
636 }
637 }
638}
639
640struct BuildingLib {
641 start_time: DInstant,
642 rebuild_load: RebuildLoadVar,
643 cancel_build: SignalOnce,
644}
645
646#[doc(hidden)]
647pub enum HotEntryExchange {
648 Request(HotNodeArgs),
649 Responding,
650 Response(Option<HotNode>),
651}
652
653#[derive(Clone)]
655pub(crate) struct HotLib {
656 manifest_dir: Txt,
657 lib: Arc<libloading::Library>,
658 hot_entry: unsafe extern "C" fn(&&str, &&'static str, &mut LocalContext, &mut HotEntryExchange),
659}
660impl PartialEq for HotLib {
661 fn eq(&self, other: &Self) -> bool {
662 Arc::ptr_eq(&self.lib, &other.lib)
663 }
664}
665impl fmt::Debug for HotLib {
666 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667 f.debug_struct("HotLib")
668 .field("manifest_dir", &self.manifest_dir)
669 .finish_non_exhaustive()
670 }
671}
672impl HotLib {
673 pub fn new(patch: &StaticPatch, manifest_dir: Txt, lib: impl AsRef<std::ffi::OsStr>) -> Result<Self, libloading::Error> {
674 unsafe {
675 let lib = libloading::Library::new(lib)?;
681
682 let init: unsafe extern "C" fn(&StaticPatch) = *lib.get(b"zng_hot_entry_init")?;
684 init(patch);
685
686 Ok(Self {
687 manifest_dir,
688 hot_entry: *lib.get(b"zng_hot_entry")?,
689 lib: Arc::new(lib),
690 })
691 }
692 }
693
694 pub fn manifest_dir(&self) -> &Txt {
696 &self.manifest_dir
697 }
698
699 pub fn instantiate(&self, hot_node_name: &'static str, ctx: &mut LocalContext, args: HotNodeArgs) -> Option<HotNode> {
700 let mut exchange = HotEntryExchange::Request(args);
701 unsafe { (self.hot_entry)(&self.manifest_dir.as_str(), &hot_node_name, ctx, &mut exchange) };
703 let mut node = match exchange {
704 HotEntryExchange::Response(n) => n,
705 _ => None,
706 };
707 if let Some(n) = &mut node {
708 n._lib = Some(self.lib.clone());
709 }
710 node
711 }
712}