hot_lib_reloader/lib_reload_events.rs
1use std::{
2 borrow::BorrowMut,
3 sync::{Arc, Condvar, Mutex, mpsc},
4 time::Duration,
5};
6
7/// Signals when the library has changed.
8/// Needs to be public as it is used in `hot_module`.
9#[derive(Clone)]
10#[non_exhaustive]
11#[doc(hidden)]
12pub enum ChangedEvent {
13 LibAboutToReload(BlockReload),
14 LibReloaded,
15}
16
17impl std::fmt::Debug for ChangedEvent {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 match self {
20 Self::LibAboutToReload(_) => write!(f, "LibAboutToReload"),
21 Self::LibReloaded => write!(f, "LibReloaded"),
22 }
23 }
24}
25
26/// See [`LibReloadObserver::wait_for_about_to_reload`].
27///
28/// [`BlockReload`] is implemented using a simple counting scheme to track how
29/// many tokens are floating around. If the number reaches 0 the update can
30/// continue.
31#[derive(Debug)]
32pub struct BlockReload {
33 pub(crate) pending: Arc<(Mutex<usize>, Condvar)>,
34}
35
36impl Clone for BlockReload {
37 fn clone(&self) -> Self {
38 **(self.pending.0.lock().unwrap().borrow_mut()) += 1;
39 Self {
40 pending: self.pending.clone(),
41 }
42 }
43}
44
45impl Drop for BlockReload {
46 fn drop(&mut self) {
47 let (counter, cond) = &*self.pending;
48 *counter.lock().unwrap() -= 1;
49 cond.notify_one();
50 }
51}
52
53/// A [`LibReloadObserver`] allows to wait for library changes. See
54/// - [`LibReloadObserver::wait_for_about_to_reload`] and
55/// - [`LibReloadObserver::wait_for_reload`]
56/// for details.
57///
58/// You can use those methods individually or in combination. In particular, if you want to serialize state before a library change happens and then deserialize / migrate it when the library update is done, using both methods in combination is quite useful. Something along the lines of:
59///
60/// ```ignore
61/// #[hot_module(dylib = "lib")]
62/// mod hot_lib {
63/// #[lib_change_subscription]
64/// pub fn subscribe() -> hot_lib_reloader::LibReloadObserver { }
65/// }
66///
67/// fn test() {
68/// let lib_observer = hot_lib::subscribe();
69///
70/// /* ... */
71///
72/// // wait for reload to begin (at this point the old version is still loaded)
73/// let update_blocker = lib_observer.wait_for_about_to_reload();
74///
75/// /* do update preparations here, e.g. serialize state */
76///
77/// // drop the blocker to allow update
78/// drop(update_blocker);
79///
80/// // wait for reload to be completed
81/// lib_observer.wait_for_reload();
82///
83/// /* new lib version is loaded now so you can e.g. restore state */
84/// }
85/// ```
86pub struct LibReloadObserver {
87 // needs to be public b/c it is used inside the [`hot_module`] macro.
88 #[doc(hidden)]
89 pub rx: mpsc::Receiver<ChangedEvent>,
90}
91
92impl LibReloadObserver {
93 /// A call to this method will do a blocking wait until the watched library is
94 /// about to change. It returns a [`BlockReload`] token. While this token is in
95 /// scope you will prevent the pending update to proceed. This is useful for
96 /// doing preparations for the update and while the old library version is still
97 /// loaded. You can for example serialize state.
98 pub fn wait_for_about_to_reload(&self) -> BlockReload {
99 loop {
100 match self.rx.recv() {
101 Ok(ChangedEvent::LibAboutToReload(block)) => return block,
102 Err(err) => {
103 panic!("LibReloadObserver failed to wait for event from reloader: {err}")
104 }
105 _ => continue,
106 }
107 }
108 }
109
110 /// Like [`Self::wait_for_about_to_reload`] but for a limited time. In case of a timeout return `None`.
111 pub fn wait_for_about_to_reload_timeout(&self, timeout: Duration) -> Option<BlockReload> {
112 loop {
113 match self.rx.recv_timeout(timeout) {
114 Ok(ChangedEvent::LibAboutToReload(block)) => return Some(block),
115 Err(_) => return None,
116 _ => continue,
117 }
118 }
119 }
120
121 /// Will do blocking wait until a new library version is loaded.
122 pub fn wait_for_reload(&self) {
123 loop {
124 match self.rx.recv() {
125 Ok(ChangedEvent::LibReloaded) => return,
126 Err(err) => {
127 panic!("LibReloadObserver failed to wait for event from reloader: {err}")
128 }
129 _ => continue,
130 }
131 }
132 }
133
134 /// Like [`Self::wait_for_reload`] but for a limited time. In case of a timeout return `false`.
135 pub fn wait_for_reload_timeout(&self, timeout: Duration) -> bool {
136 loop {
137 match self.rx.recv_timeout(timeout) {
138 Ok(ChangedEvent::LibReloaded) => return true,
139 Err(_) => return false,
140 _ => continue,
141 }
142 }
143 }
144}
145
146/// Needs to be public as it is used in the `hot_module` macro.
147#[derive(Default)]
148#[doc(hidden)]
149pub struct LibReloadNotifier {
150 subscribers: Arc<Mutex<Vec<mpsc::Sender<ChangedEvent>>>>,
151}
152
153impl LibReloadNotifier {
154 /// Needs to be public as it is used in the `hot_module` macro.
155 ///
156 /// The count used here represents [`BlockReload`] tokens that are still
157 /// floating around. When a token is dropped the count is decremented and
158 /// the condvar signaled.
159 #[doc(hidden)]
160 pub fn send_about_to_reload_event_and_wait_for_blocks(&self) {
161 let pending = Arc::new((Mutex::new(1), std::sync::Condvar::new()));
162 let block = BlockReload {
163 pending: pending.clone(),
164 };
165 self.notify(ChangedEvent::LibAboutToReload(block));
166 let (counter, cond) = &*pending;
167 log::trace!(
168 "about-to-change library event, waiting for {}",
169 counter.lock().unwrap()
170 );
171 let _guard = cond
172 .wait_while(counter.lock().unwrap(), |pending| {
173 log::trace!(
174 "about-to-change library event, now waiting for {}",
175 *pending
176 );
177 *pending > 0
178 })
179 .unwrap();
180 }
181
182 #[doc(hidden)]
183 pub fn send_reloaded_event(&self) {
184 self.notify(ChangedEvent::LibReloaded);
185 }
186
187 fn notify(&self, evt: ChangedEvent) {
188 if let Ok(mut subscribers) = self.subscribers.try_lock() {
189 let n = subscribers.len();
190 log::trace!("sending {evt:?} to {n} subscribers");
191 // keep only those subscribers that are still around and kicking.
192 subscribers.retain(|tx| tx.send(evt.clone()).is_ok());
193 let removed = n - subscribers.len();
194 if removed > 0 {
195 log::debug!(
196 "removing {removed} subscriber{}",
197 if removed == 1 { "" } else { "s" }
198 );
199 }
200 }
201 }
202
203 /// Needs to be public as it is used in the `hot_module` macro.
204 ///
205 /// Create a [ChangedEvent] receiver that gets signalled when the library
206 /// changes.
207 #[doc(hidden)]
208 pub fn subscribe(&mut self) -> LibReloadObserver {
209 log::trace!("subscribe to lib change");
210 let (tx, rx) = mpsc::channel();
211 let mut subscribers = self.subscribers.lock().unwrap();
212 subscribers.push(tx);
213 LibReloadObserver { rx }
214 }
215}