1use crate::error::{LoaderError, Result};
4use crate::lock;
5use crate::registry::{PluginRegistry, WithInject};
6use cordis::{Config, Context, EffectHandle, EventOptions, Fiber, FiberState, PluginHandle};
7use cordis_include::{Entry, EntryOptions, EntryTree, LoaderFile, Node, PluginResolver, TreeDiff};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::sync::{Arc, Mutex, Weak};
11
12#[derive(Clone, Default)]
14pub struct LoaderConfig {
15 pub filename: PathBuf,
17 pub initial: Option<cordis_include::Document>,
19 pub registry: Option<PluginRegistry>,
22}
23
24impl LoaderConfig {
25 pub fn new(filename: impl Into<PathBuf>) -> Self {
27 Self {
28 filename: filename.into(),
29 initial: None,
30 registry: None,
31 }
32 }
33
34 pub fn with_initial(mut self, initial: cordis_include::Document) -> Self {
36 self.initial = Some(initial);
37 self
38 }
39
40 pub fn with_registry(mut self, registry: PluginRegistry) -> Self {
42 self.registry = Some(registry);
43 self
44 }
45}
46
47struct LoaderState {
49 entries: HashMap<u64, Entry>,
51 operating: u16,
54 last_error: Option<String>,
56 _keep_alive: Vec<EffectHandle>,
58}
59
60#[derive(Clone)]
62pub struct Loader {
63 pub(crate) inner: Arc<LoaderInner>,
64}
65
66pub(crate) struct LoaderInner {
67 root: Context,
68 file: LoaderFile,
69 tree: EntryTree,
70 registry: Mutex<PluginRegistry>,
71 state: Mutex<LoaderState>,
72}
73
74pub struct LoaderHandle {
79 inner: Weak<LoaderInner>,
80}
81
82impl LoaderHandle {
83 pub fn upgrade(&self) -> Option<Loader> {
85 self.inner.upgrade().map(|inner| Loader { inner })
86 }
87}
88
89impl Loader {
90 pub fn open(root: &Context, config: LoaderConfig) -> Result<Loader> {
97 let file = LoaderFile::open(&config.filename)?;
98 if !file.path().exists() {
99 if let Some(initial) = &config.initial {
100 file.write(initial)?;
101 }
102 }
103 let tree = EntryTree::new();
104 tree.update(file.read()?.entries)?;
105 let inner = Arc::new(LoaderInner {
106 root: root.clone(),
107 file,
108 tree,
109 registry: Mutex::new(config.registry.unwrap_or_default()),
110 state: Mutex::new(LoaderState {
111 entries: HashMap::new(),
112 operating: 0,
113 last_error: None,
114 _keep_alive: Vec::new(),
115 }),
116 });
117
118 let weak = Arc::downgrade(&inner);
121 let status = root.events().on(
122 "internal/status",
123 move |event| {
124 if let Some(inner) = weak.upgrade() {
125 handle_status(&inner, &event)?;
126 }
127 Ok(None)
128 },
129 EventOptions {
130 global: true,
131 ..EventOptions::default()
132 },
133 )?;
134 let service = root.provide_arc(
135 "loader",
136 Arc::new(LoaderHandle {
137 inner: Arc::downgrade(&inner),
138 }),
139 )?;
140 lock(&inner.state)._keep_alive = vec![status, service];
141
142 let loader = Loader { inner };
143 loader.start_all();
144 Ok(loader)
145 }
146
147 pub fn context(&self) -> &Context {
149 &self.inner.root
150 }
151
152 pub fn tree(&self) -> &EntryTree {
154 &self.inner.tree
155 }
156
157 pub fn file(&self) -> &LoaderFile {
159 &self.inner.file
160 }
161
162 pub fn registry(&self) -> PluginRegistry {
166 lock(&self.inner.registry).clone()
167 }
168
169 pub fn register_plugin<P: cordis::Plugin>(&self, plugin: P) {
172 lock(&self.inner.registry).register_plugin(plugin);
173 }
174
175 pub fn register<F>(&self, name: impl Into<String>, factory: F)
177 where
178 F: Fn() -> PluginHandle + Send + Sync + 'static,
179 {
180 lock(&self.inner.registry).register(name, factory);
181 }
182
183 pub fn last_error(&self) -> Option<String> {
185 lock(&self.inner.state).last_error.clone()
186 }
187
188 pub fn locate(&self, fiber: &Fiber) -> Option<Entry> {
190 let state = lock(&self.inner.state);
191 if let Some(uid) = fiber.uid() {
192 return state.entries.get(&uid).cloned();
193 }
194 state
195 .entries
196 .values()
197 .find(|entry| entry.fiber().is_some_and(|started| started.ptr_eq(fiber)))
198 .cloned()
199 }
200
201 fn start_all(&self) {
203 for entry in self.inner.tree.entries() {
204 if let Err(error) = start_entry(&self.inner, &entry) {
205 self.record_error(error);
206 }
207 }
208 }
209
210 pub fn reload(&self) -> Result<TreeDiff> {
218 let inner = &self.inner;
219 let _suspend = inner.file.suspend();
220 let document = inner.file.read()?;
221 let diff = inner.tree.update(document.entries)?;
222
223 for entry in &diff.removed {
224 if let Err(error) = stop_entry(inner, entry) {
225 self.record_error(error);
226 }
227 }
228 for entry in &diff.moved {
229 if let Err(error) = stop_entry(inner, entry) {
230 self.record_error(error);
231 }
232 }
233 for entry in &diff.updated {
234 if let Err(error) = patch_entry(inner, entry) {
235 self.record_error(error);
236 }
237 }
238 for entry in &diff.created {
239 if let Err(error) = start_entry(inner, entry) {
240 self.record_error(error);
241 }
242 }
243 drop(_suspend);
244
245 if !diff.created.is_empty() {
248 write_back(inner)?;
249 }
250 Ok(diff)
251 }
252
253 pub fn update_config(&self, id: &str, config: Node) -> Result<()> {
256 let inner = &self.inner;
257 let entry = inner.tree.resolve(id).ok_or_else(|| {
258 LoaderError::Include(cordis_include::IncludeError::EntryNotFound { id: id.to_owned() })
259 })?;
260 if let Some(fiber) = entry.fiber() {
261 fiber.update_value(Config::new(config.clone()))?;
262 }
263 let mut options = entry_options_with_children(&entry);
264 options.config = Some(config);
265 inner
266 .tree
267 .update_entry(&entry.path(), options, None, None)?;
268 write_back(inner)
269 }
270
271 pub fn dispose(&self) -> Result<()> {
274 let inner = &self.inner;
275 for entry in inner.tree.top_level() {
276 if let Err(error) = stop_entry(inner, &entry) {
277 self.record_error(error);
278 }
279 }
280 Ok(())
281 }
282
283 #[cfg(feature = "watch")]
287 pub fn watch(&self) -> Result<cordis_include::FileWatcher> {
288 let loader = self.clone();
289 self.inner
290 .file
291 .watch(move || {
292 if let Err(error) = loader.reload() {
293 loader.record_error(error);
294 }
295 })
296 .map_err(LoaderError::Include)
297 }
298
299 fn record_error(&self, error: LoaderError) {
300 lock(&self.inner.state).last_error = Some(error.to_string());
301 }
302}
303
304struct OperatingGuard<'a> {
307 state: &'a Mutex<LoaderState>,
308}
309
310impl<'a> OperatingGuard<'a> {
311 fn new(state: &'a Mutex<LoaderState>) -> Self {
312 lock(state).operating += 1;
313 Self { state }
314 }
315}
316
317impl Drop for OperatingGuard<'_> {
318 fn drop(&mut self) {
319 let mut state = lock(self.state);
320 state.operating = state.operating.saturating_sub(1);
321 }
322}
323
324fn start_entry(inner: &LoaderInner, entry: &Entry) -> Result<()> {
326 if !entry.enabled() || entry.fiber().is_some() {
327 return Ok(());
328 }
329 let name = entry.name();
330 let handle: PluginHandle = lock(&inner.registry)
331 .resolve(&name)
332 .map_err(LoaderError::Cordis)?;
333 let inject = entry.options().inject;
334 let handle = WithInject::wrap(handle, inject);
335 let config = entry.resolved_config()?.unwrap_or(Node::Null);
336 let parent_ctx = entry
337 .parent()
338 .and_then(|parent| parent.fiber())
339 .and_then(|fiber| fiber.context())
340 .unwrap_or_else(|| inner.root.clone());
341 let fiber = parent_ctx.plugin(handle, config);
342 entry.set_fiber(Some(fiber.clone()));
343 if let Some(uid) = fiber.uid() {
344 lock(&inner.state).entries.insert(uid, entry.clone());
345 }
346 Ok(())
347}
348
349fn stop_entry(inner: &LoaderInner, entry: &Entry) -> Result<()> {
352 for child in entry.children() {
353 stop_entry(inner, &child)?;
354 }
355 let Some(fiber) = entry.fiber() else {
356 return Ok(());
357 };
358 entry.set_fiber(None);
359 if let Some(uid) = fiber.uid() {
360 lock(&inner.state).entries.remove(&uid);
361 }
362 let _guard = OperatingGuard::new(&inner.state);
363 fiber.dispose().map_err(LoaderError::Cordis)
364}
365
366fn patch_entry(inner: &LoaderInner, entry: &Entry) -> Result<()> {
370 if !entry.enabled() {
371 return stop_entry(inner, entry);
372 }
373 let Some(fiber) = entry.fiber() else {
374 return start_entry(inner, entry);
375 };
376 let options = entry.options();
377 let inject_changed =
378 fiber.inject().names().collect::<Vec<_>>() != options.inject.iter().collect::<Vec<_>>();
379 if fiber.name() != options.name || inject_changed {
380 stop_entry(inner, entry)?;
381 return start_entry(inner, entry);
382 }
383 let new_config = entry.resolved_config()?.unwrap_or(Node::Null);
384 let current = fiber
385 .config()
386 .downcast::<Node>()
387 .ok()
388 .map(|node| (*node).clone());
389 if current.as_ref() != Some(&new_config) {
390 fiber.update_value(Config::new(new_config))?;
391 }
392 Ok(())
393}
394
395fn entry_options_with_children(entry: &Entry) -> EntryOptions {
398 let mut options = entry.options();
399 options.group = entry
400 .children()
401 .iter()
402 .map(entry_options_with_children)
403 .collect();
404 options
405}
406
407fn write_back(inner: &LoaderInner) -> Result<()> {
409 let mut document = inner.file.read()?;
410 document.entries = inner.tree.serialize();
411 inner.file.write(&document)?;
412 Ok(())
413}
414
415fn handle_status(inner: &LoaderInner, event: &cordis::Event) -> cordis::EventResult {
419 let Some(fiber) = event.arg::<Fiber>(0).ok().flatten() else {
420 return Ok(None);
421 };
422 if fiber.state() != FiberState::Disposed {
423 return Ok(None);
424 }
425 if lock(&inner.state).operating > 0 {
426 return Ok(None);
427 }
428 let Some(entry) = lock(&inner.state)
429 .entries
430 .values()
431 .find(|entry| entry.fiber().is_some_and(|started| started.ptr_eq(&fiber)))
432 .cloned()
433 else {
434 return Ok(None);
435 };
436 if let Err(error) = persist_self_dispose(inner, &entry) {
437 lock(&inner.state).last_error = Some(error.to_string());
438 }
439 Ok(None)
440}
441
442fn persist_self_dispose(inner: &LoaderInner, entry: &Entry) -> Result<()> {
444 {
445 let mut state = lock(&inner.state);
446 let key = state
447 .entries
448 .iter()
449 .find(|(_, mapped)| Entry::ptr_eq(mapped, entry))
450 .map(|(uid, _)| *uid);
451 if let Some(uid) = key {
452 state.entries.remove(&uid);
453 }
454 }
455 entry.set_fiber(None);
456 let mut options = entry_options_with_children(entry);
457 options.disabled = true;
458 inner
459 .tree
460 .update_entry(&entry.path(), options, None, None)?;
461 write_back(inner)
462}