1use crate::{admit, Admission};
3use std::{
4 collections::{BTreeMap, BTreeSet},
5 marker::PhantomData,
6 rc::Rc,
7};
8pub type SourceSnapshot = BTreeMap<String, String>;
9pub struct PatchSupervisor {
12 baseline: SourceSnapshot,
13 registered: BTreeMap<String, BTreeSet<String>>,
14 view_builders: BTreeMap<(String, String), BTreeSet<String>>,
15 revision: u64,
16 accepted_table: Option<subsecond::JumpTable>,
17 restart_required: Option<String>,
18 identity: Rc<()>,
19 _ui_thread: PhantomData<Rc<()>>,
20}
21#[derive(Debug)]
22pub struct AdmittedPatch {
23 owner: Rc<()>,
24 baseline_revision: u64,
25 sources: SourceSnapshot,
26 pub functions: Vec<String>,
27 views: Vec<String>,
28}
29impl AdmittedPatch {
30 pub fn views_to_reapply(&self) -> &[String] {
33 &self.views
34 }
35}
36impl PatchSupervisor {
37 pub fn new(baseline: SourceSnapshot, registered: BTreeMap<String, BTreeSet<String>>) -> Self {
38 Self {
39 baseline,
40 registered,
41 view_builders: BTreeMap::new(),
42 revision: 0,
43 accepted_table: None,
44 restart_required: None,
45 identity: Rc::new(()),
46 _ui_thread: PhantomData,
47 }
48 }
49 pub fn with_registrations(
52 baseline: SourceSnapshot,
53 registrations: impl IntoIterator<Item = crate::FunctionRegistration>,
54 ) -> Result<Self, String> {
55 let mut supervisor = Self::new(baseline, BTreeMap::new());
56 for registration in registrations {
57 let source = supervisor
58 .baseline
59 .get(®istration.source)
60 .ok_or_else(|| format!("Registered source is missing: {}", registration.source))?;
61 crate::admission::validate_registration(source, ®istration.function)?;
62 if !supervisor
63 .registered
64 .entry(registration.source.clone())
65 .or_default()
66 .insert(registration.function.clone())
67 {
68 return Err(format!(
69 "Duplicate function registration: {}::{}",
70 registration.source, registration.function
71 ));
72 }
73 if let Some(views) = registration.views {
74 if views.is_empty() || views.iter().any(|view| view.trim().is_empty()) {
75 return Err("A view builder requires nonempty view identifiers".into());
76 }
77 supervisor
78 .view_builders
79 .insert((registration.source, registration.function), views);
80 }
81 }
82 Ok(supervisor)
83 }
84 pub fn revision(&self) -> u64 {
85 self.revision
86 }
87 pub fn admit(&self, sources: SourceSnapshot) -> Result<AdmittedPatch, String> {
88 if let Some(reason) = &self.restart_required {
89 return Err(reason.clone());
90 }
91 if self.baseline.keys().ne(sources.keys()) {
92 return Err("Build input set changed; restart required".into());
93 }
94 let mut functions = Vec::new();
95 let mut views = BTreeSet::new();
96 for (path, before) in &self.baseline {
97 let after = &sources[path];
98 if before == after {
99 continue;
100 }
101 let registered = self
102 .registered
103 .get(path)
104 .ok_or_else(|| format!("Unpatchable build input changed: {path}"))?;
105 match admit(before, after, registered) {
106 Admission::Unchanged => (),
107 Admission::Patch { functions: changed } => {
108 for name in &changed {
109 if let Some(affected) =
110 self.view_builders.get(&(path.clone(), name.clone()))
111 {
112 views.extend(affected.iter().cloned());
113 }
114 }
115 functions.extend(changed.into_iter().map(|name| format!("{path}::{name}")))
116 }
117 Admission::RestartRequired(reason) => return Err(format!("{path}: {reason}")),
118 }
119 }
120 if functions.is_empty() {
121 return Err("No changed patchable functions".into());
122 }
123 Ok(AdmittedPatch {
124 owner: self.identity.clone(),
125 baseline_revision: self.revision,
126 sources,
127 functions,
128 views: views.into_iter().collect(),
129 })
130 }
131 pub unsafe fn apply(
142 &mut self,
143 patch: AdmittedPatch,
144 current: &SourceSnapshot,
145 table: subsecond::JumpTable,
146 ) -> Result<u64, String> {
147 if !patch.views.is_empty() {
148 return Err("Registered views require an apply_with transaction".into());
149 }
150 unsafe { self.apply_with(patch, current, table, || Ok(())) }
152 }
153
154 pub unsafe fn apply_with(
170 &mut self,
171 patch: AdmittedPatch,
172 current: &SourceSnapshot,
173 table: subsecond::JumpTable,
174 transaction: impl FnOnce() -> Result<(), String>,
175 ) -> Result<u64, String> {
176 self.validate(&patch, current)?;
177 if crate::in_callback() {
178 return Err("Patch application must wait until the callback returns".into());
179 }
180 let accepted = table.clone();
181 let rollback = self.accepted_table.clone().unwrap_or_else(|| {
182 let mut original = table.clone();
186 original.map.clear();
187 original
188 });
189 unsafe { subsecond::apply_patch(table) }.map_err(|error| format!("{error:?}"))?;
191 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(transaction));
192 if !matches!(&outcome, Ok(Ok(()))) {
193 if let Err(error) = unsafe { subsecond::apply_patch(rollback) } {
196 let reason = format!("Restart required: dispatch rollback failed: {error:?}");
197 self.restart_required = Some(reason.clone());
198 return Err(reason);
199 }
200 return match outcome {
201 Ok(Err(error)) => Err(error),
202 Err(payload) => std::panic::resume_unwind(payload),
203 Ok(Ok(())) => unreachable!(),
204 };
205 }
206 self.accepted_table = Some(accepted);
207 self.baseline = patch.sources;
208 self.revision += 1;
209 Ok(self.revision)
210 }
211 fn validate(&self, patch: &AdmittedPatch, current: &SourceSnapshot) -> Result<(), String> {
212 if let Some(reason) = &self.restart_required {
213 return Err(reason.clone());
214 }
215 if !Rc::ptr_eq(&patch.owner, &self.identity) {
216 return Err("Patch belongs to another supervisor".into());
217 }
218 if patch.baseline_revision != self.revision || &patch.sources != current {
219 return Err("Superseded patch".into());
220 }
221 Ok(())
222 }
223
224 pub fn restart_required(&self) -> Option<&str> {
227 self.restart_required.as_deref()
228 }
229}
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::FunctionRegistration;
234
235 #[test]
236 fn only_changed_builders_request_their_registered_views() {
237 let source = "fn click() -> i32 { 1 } fn build() -> i32 { 2 } fn detail() -> i32 { 3 }";
238 let baseline = BTreeMap::from([("app.rs".into(), source.into())]);
239 let supervisor = PatchSupervisor::with_registrations(
240 baseline,
241 [
242 FunctionRegistration::callback("app.rs", "click"),
243 FunctionRegistration::view_builder(
244 "app.rs",
245 "build",
246 ["main".into(), "detail".into()],
247 ),
248 FunctionRegistration::view_builder("app.rs", "detail", ["detail".into()]),
249 ],
250 )
251 .unwrap();
252 let callback = BTreeMap::from([("app.rs".into(), source.replace("{ 1 }", "{ 4 }"))]);
253 assert!(supervisor
254 .admit(callback)
255 .unwrap()
256 .views_to_reapply()
257 .is_empty());
258 let builders = BTreeMap::from([(
259 "app.rs".into(),
260 source.replace("{ 2 }", "{ 5 }").replace("{ 3 }", "{ 6 }"),
261 )]);
262 let ticket = supervisor.admit(builders).unwrap();
263 assert_eq!(
264 ticket.views_to_reapply(),
265 &["detail".to_string(), "main".to_string()]
266 );
267 assert_eq!(ticket.functions, ["app.rs::build", "app.rs::detail"]);
268 }
269
270 #[test]
271 fn invalid_or_ambiguous_registrations_fail_before_admission() {
272 let baseline = BTreeMap::from([(
273 "app.rs".into(),
274 "fn click() {} async fn task() {} fn generic<T>() {} fn closure() { let f = || 1; }"
275 .into(),
276 )]);
277 for registrations in [
278 vec![FunctionRegistration::callback("absent.rs", "click")],
279 vec![FunctionRegistration::callback("app.rs", "absent")],
280 vec![FunctionRegistration::callback("app.rs", "task")],
281 vec![FunctionRegistration::callback("app.rs", "generic")],
282 vec![FunctionRegistration::callback("app.rs", "closure")],
283 vec![
284 FunctionRegistration::callback("app.rs", "click"),
285 FunctionRegistration::callback("app.rs", "click"),
286 ],
287 vec![FunctionRegistration::view_builder("app.rs", "click", [])],
288 vec![FunctionRegistration::view_builder(
289 "app.rs",
290 "click",
291 [" ".into()],
292 )],
293 ] {
294 assert!(PatchSupervisor::with_registrations(baseline.clone(), registrations).is_err());
295 }
296 }
297 #[test]
298 fn admission_tickets_are_bound_to_their_supervisor_and_revision() {
299 let before = BTreeMap::from([("main.rs".into(), "fn click() {}".into())]);
300 let registered = BTreeMap::from([("main.rs".into(), BTreeSet::from(["click".into()]))]);
301 let mut owner = PatchSupervisor::new(before.clone(), registered.clone());
302 let other = PatchSupervisor::new(before, registered);
303 let after = BTreeMap::from([("main.rs".into(), "fn click() { let x = 2; }".into())]);
304 let ticket = owner.admit(after.clone()).unwrap();
305 assert!(owner.validate(&ticket, &after).is_ok());
306 assert!(other
307 .validate(&ticket, &after)
308 .unwrap_err()
309 .contains("another supervisor"));
310 owner.revision += 1;
311 assert!(owner
312 .validate(&ticket, &after)
313 .unwrap_err()
314 .contains("Superseded"));
315 }
316 #[test]
317 fn rejects_changed_dependency_before_loading() {
318 let old = BTreeMap::from([
319 ("main.rs".into(), "fn click() {}".into()),
320 ("Cargo.toml".into(), "original".into()),
321 ]);
322 let supervisor = PatchSupervisor::new(
323 old.clone(),
324 BTreeMap::from([("main.rs".into(), BTreeSet::from(["click".into()]))]),
325 );
326 let mut new = old;
327 new.insert("main.rs".into(), "fn click() { let x = 2; }".into());
328 assert!(supervisor.admit(new.clone()).is_ok());
329 new.insert("Cargo.toml".into(), "changed".into());
330 assert!(supervisor.admit(new).unwrap_err().contains("Unpatchable"));
331 }
332}