1use crate::error::{LoaderError, Result};
4use crate::lock;
5use crate::registry::{PluginRegistry, WithInject};
6use cordis::{
7 Config, Context, CordisError, EffectHandle, ErrorCode, EventOptions, Fiber, FiberState,
8 PluginHandle, Value,
9};
10use cordis_include::{Entry, EntryOptions, EntryTree, LoaderFile, Node, PluginResolver, TreeDiff};
11use std::collections::{HashMap, HashSet};
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, Condvar, Mutex, Weak};
14use std::thread::ThreadId;
15use std::time::Duration;
16
17#[derive(Clone, Default)]
19pub struct LoaderConfig {
20 pub filename: PathBuf,
22 pub initial: Option<cordis_include::Document>,
24 pub registry: Option<PluginRegistry>,
27 pub write_debounce: Option<Duration>,
30}
31
32impl LoaderConfig {
33 pub fn new(filename: impl Into<PathBuf>) -> Self {
35 Self {
36 filename: filename.into(),
37 initial: None,
38 registry: None,
39 write_debounce: None,
40 }
41 }
42
43 pub fn with_initial(mut self, initial: cordis_include::Document) -> Self {
45 self.initial = Some(initial);
46 self
47 }
48
49 pub fn with_registry(mut self, registry: PluginRegistry) -> Self {
51 self.registry = Some(registry);
52 self
53 }
54
55 pub fn with_write_debounce(mut self, delay: Duration) -> Self {
58 self.write_debounce = Some(delay);
59 self
60 }
61}
62
63struct LoaderState {
65 entries: HashMap<u64, Entry>,
67 operating: u16,
70 last_error: Option<String>,
72 _keep_alive: Vec<EffectHandle>,
74}
75
76#[derive(Clone)]
78pub struct Loader {
79 pub(crate) inner: Arc<LoaderInner>,
80}
81
82pub(crate) struct LoaderInner {
83 root: Context,
84 file: LoaderFile,
85 tree: EntryTree,
86 registry: Mutex<PluginRegistry>,
87 state: Mutex<LoaderState>,
88 operation: OperationLock,
97 imports: Mutex<HashMap<PathBuf, LoaderFile>>,
99 #[cfg(feature = "watch")]
101 watched: Mutex<HashSet<PathBuf>>,
102 #[cfg(feature = "watch")]
104 watchers: Mutex<Vec<cordis_include::FileWatcher>>,
105 write_debounce: Mutex<Option<Duration>>,
107}
108
109pub struct LoaderHandle {
114 inner: Weak<LoaderInner>,
115}
116
117impl LoaderHandle {
118 pub fn upgrade(&self) -> Option<Loader> {
120 self.inner.upgrade().map(|inner| Loader { inner })
121 }
122}
123
124impl Loader {
125 pub fn open(root: &Context, config: LoaderConfig) -> Result<Loader> {
132 let file = LoaderFile::open(&config.filename)?;
133 if !file.path().exists() {
134 if let Some(initial) = &config.initial {
135 file.write(initial)?;
136 }
137 }
138 let mut imports = HashMap::new();
142 let mut errors = Vec::new();
143 let (composed, _dirty) = compose(
144 &file,
145 &mut imports,
146 &mut HashSet::new(),
147 &mut HashSet::new(),
148 &mut errors,
149 )?;
150 let inner = Arc::new(LoaderInner {
151 root: root.clone(),
152 file,
153 tree: EntryTree::new(),
154 registry: Mutex::new(config.registry.unwrap_or_default()),
155 state: Mutex::new(LoaderState {
156 entries: HashMap::new(),
157 operating: 0,
158 last_error: errors.pop(),
159 _keep_alive: Vec::new(),
160 }),
161 operation: OperationLock::default(),
162 imports: Mutex::new(imports),
163 #[cfg(feature = "watch")]
164 watched: Mutex::new(HashSet::new()),
165 #[cfg(feature = "watch")]
166 watchers: Mutex::new(Vec::new()),
167 write_debounce: Mutex::new(config.write_debounce),
168 });
169 inner.tree.update(composed)?;
170 let weak = Arc::downgrade(&inner);
176 let status = root.events().on(
177 "internal/status",
178 move |event| {
179 if let Some(inner) = weak.upgrade() {
180 handle_status(&inner, &event)?;
181 }
182 Ok(None)
183 },
184 EventOptions {
185 global: true,
186 ..EventOptions::default()
187 },
188 )?;
189 let service = root.provide_arc(
190 "loader",
191 Arc::new(LoaderHandle {
192 inner: Arc::downgrade(&inner),
193 }),
194 )?;
195 lock(&inner.state)._keep_alive = vec![status, service];
196
197 let loader = Loader { inner };
198 loader.start_all();
199 Ok(loader)
200 }
201
202 pub fn context(&self) -> &Context {
204 &self.inner.root
205 }
206
207 pub fn tree(&self) -> &EntryTree {
209 &self.inner.tree
210 }
211
212 pub fn file(&self) -> &LoaderFile {
214 &self.inner.file
215 }
216
217 pub fn registry(&self) -> PluginRegistry {
221 lock(&self.inner.registry).clone()
222 }
223
224 pub fn register_plugin<P: cordis::Plugin>(&self, plugin: P) {
227 lock(&self.inner.registry).register_plugin(plugin);
228 }
229
230 pub fn register<F>(&self, name: impl Into<String>, factory: F)
232 where
233 F: Fn() -> PluginHandle + Send + Sync + 'static,
234 {
235 lock(&self.inner.registry).register(name, factory);
236 }
237
238 pub fn last_error(&self) -> Option<String> {
240 lock(&self.inner.state).last_error.clone()
241 }
242
243 pub fn set_write_debounce(&self, delay: Option<Duration>) {
246 *lock(&self.inner.write_debounce) = delay;
247 }
248
249 pub fn locate(&self, fiber: &Fiber) -> Option<Entry> {
251 let state = lock(&self.inner.state);
252 if let Some(uid) = fiber.uid() {
253 return state.entries.get(&uid).cloned();
254 }
255 state
256 .entries
257 .values()
258 .find(|entry| entry.fiber().is_some_and(|started| started.ptr_eq(fiber)))
259 .cloned()
260 }
261
262 fn start_all(&self) {
264 for entry in self.inner.tree.entries() {
265 if let Err(error) = start_entry(&self.inner, &entry) {
266 self.record_error(error);
267 }
268 }
269 }
270
271 pub fn reload(&self) -> Result<TreeDiff> {
285 let inner = &self.inner;
286 let _operation = inner.operation.guard();
289 let mut imports = HashMap::new();
290 let mut errors = Vec::new();
291 let (composed, dirty) = match compose(
292 &inner.file,
293 &mut imports,
294 &mut HashSet::new(),
295 &mut HashSet::new(),
296 &mut errors,
297 ) {
298 Ok(composed) => composed,
299 Err(error) => {
300 self.record_error(&error);
305 return Err(error.into());
306 }
307 };
308 for error in errors {
309 self.record_error(LoaderError::Include(
310 cordis_include::IncludeError::Message { message: error },
311 ));
312 }
313 let diff = inner.tree.update(composed)?;
314 *lock(&inner.imports) = imports;
315
316 for removed in &diff.removed {
317 if let Err(error) = stop_entry(inner, &removed.entry) {
318 self.record_error(error);
319 }
320 }
321 for entry in &diff.moved {
322 if let Err(error) = stop_entry(inner, entry) {
323 self.record_error(error);
324 }
325 }
326 for entry in &diff.redefined {
327 if let Err(error) = stop_entry(inner, entry) {
328 self.record_error(error);
329 }
330 }
331 for entry in &diff.updated {
332 if let Err(error) = patch_entry(inner, entry) {
333 self.record_error(error);
334 }
335 }
336 for entry in &diff.created {
337 if let Err(error) = start_entry(inner, entry) {
338 self.record_error(error);
339 }
340 }
341
342 let mut restarts: Vec<&Entry> = diff
347 .moved
348 .iter()
349 .chain(&diff.redefined)
350 .chain(diff.updated.iter().filter(|entry| entry.fiber().is_none()))
351 .collect();
352 restarts.sort_by_key(|entry| entry_depth(entry));
353 for entry in restarts {
354 if let Err(error) = start_subtree(inner, entry) {
355 self.record_error(error);
356 }
357 }
358
359 if dirty {
362 write_back(inner)?;
363 }
364 #[cfg(feature = "watch")]
365 self.arm_import_watchers();
366 Ok(diff)
367 }
368
369 pub fn update_config(&self, id: &str, config: Node) -> Result<()> {
372 let inner = &self.inner;
373 let _operation = inner.operation.guard();
377 let entry = inner.tree.resolve(id).ok_or_else(|| {
378 LoaderError::Include(cordis_include::IncludeError::EntryNotFound { id: id.to_owned() })
379 })?;
380 if let Some(fiber) = entry.fiber() {
381 fiber.update_value(Config::new(config.clone()))?;
382 }
383 let mut options = entry_options_with_children(&entry);
384 options.config = Some(config.clone());
385 inner
386 .tree
387 .update_entry(&entry.path(), options, None, None)?;
388 write_back(inner)?;
389 emit(
390 inner,
391 crate::events::CONFIG_UPDATE,
392 vec![Value::new(entry), Value::new(config)],
393 );
394 Ok(())
395 }
396
397 pub fn dispose(&self) -> Result<()> {
402 let inner = &self.inner;
403 let _operation = inner.operation.guard();
406 for entry in inner.tree.top_level() {
407 if let Err(error) = stop_entry(inner, &entry) {
408 self.record_error(error);
409 }
410 }
411 #[cfg(feature = "watch")]
412 {
413 lock(&inner.watched).clear();
414 lock(&inner.watchers).clear();
415 }
416 let keep_alive = std::mem::take(&mut lock(&inner.state)._keep_alive);
417 for effect in &keep_alive {
418 if let Err(error) = effect.dispose() {
419 self.record_error(LoaderError::Cordis(error));
420 }
421 }
422 Ok(())
423 }
424
425 #[cfg(feature = "watch")]
429 pub fn watch(&self) -> Result<cordis_include::FileWatcher> {
430 let loader = self.clone();
431 let watcher = self
432 .inner
433 .file
434 .watch(move || {
435 if let Err(error) = loader.reload() {
436 loader.record_error(error);
437 }
438 })
439 .map_err(LoaderError::Include)?;
440 let main_path = std::fs::canonicalize(self.inner.file.path())
441 .unwrap_or_else(|_| self.inner.file.path().to_path_buf());
442 lock(&self.inner.watched).insert(main_path);
443 self.arm_import_watchers();
444 Ok(watcher)
445 }
446
447 #[cfg(feature = "watch")]
450 fn arm_import_watchers(&self) {
451 for (path, file) in lock(&self.inner.imports).clone() {
452 if lock(&self.inner.watched).contains(&path) {
453 continue;
454 }
455 let loader = self.clone();
456 match file.watch(move || {
457 if let Err(error) = loader.reload() {
458 loader.record_error(error);
459 }
460 }) {
461 Ok(watcher) => {
462 lock(&self.inner.watched).insert(path);
463 lock(&self.inner.watchers).push(watcher);
464 }
465 Err(error) => self.record_error(LoaderError::Include(error)),
466 }
467 }
468 }
469
470 fn record_error(&self, error: impl std::fmt::Display) {
471 lock(&self.inner.state).last_error = Some(error.to_string());
472 }
473}
474
475struct OperatingGuard<'a> {
478 state: &'a Mutex<LoaderState>,
479}
480
481impl<'a> OperatingGuard<'a> {
482 fn new(state: &'a Mutex<LoaderState>) -> Self {
483 lock(state).operating += 1;
484 Self { state }
485 }
486}
487
488impl Drop for OperatingGuard<'_> {
489 fn drop(&mut self) {
490 let mut state = lock(self.state);
491 state.operating = state.operating.saturating_sub(1);
492 }
493}
494
495#[derive(Default)]
504struct OperationLock {
505 state: Mutex<OperationState>,
506 released: Condvar,
507}
508
509#[derive(Default)]
510struct OperationState {
511 owner: Option<ThreadId>,
512 depth: usize,
513}
514
515impl OperationLock {
516 fn guard(&self) -> OperationGuard<'_> {
518 let current = std::thread::current().id();
519 let mut state = lock(&self.state);
520 loop {
521 if state.owner.is_none_or(|owner| owner == current) {
522 state.owner = Some(current);
523 state.depth += 1;
524 return OperationGuard { lock: self };
525 }
526 let guard = self
527 .released
528 .wait(state)
529 .unwrap_or_else(|error| error.into_inner());
530 state = guard;
531 }
532 }
533}
534
535struct OperationGuard<'a> {
536 lock: &'a OperationLock,
537}
538
539impl Drop for OperationGuard<'_> {
540 fn drop(&mut self) {
541 let mut state = lock(&self.lock.state);
542 state.depth = state.depth.saturating_sub(1);
543 if state.depth == 0 {
544 state.owner = None;
545 drop(state);
546 self.lock.released.notify_all();
547 }
548 }
549}
550
551fn emit(inner: &LoaderInner, name: &str, args: Vec<Value>) {
554 if let Err(error) = inner.root.events().emit(name, args) {
555 lock(&inner.state).last_error = Some(format!("{name} listener failed: {error}"));
556 }
557}
558
559fn start_entry(inner: &LoaderInner, entry: &Entry) -> Result<()> {
561 if !entry.enabled() || entry.fiber().is_some() {
562 return Ok(());
563 }
564 let name = entry.name();
565 let handle: PluginHandle = lock(&inner.registry)
566 .resolve(&name)
567 .map_err(LoaderError::Cordis)?;
568 let inject = entry.options().inject;
569 let handle = WithInject::wrap(handle, inject);
570 let config = entry.resolved_config()?.unwrap_or(Node::Null);
571 let parent_ctx = entry
572 .parent()
573 .and_then(|parent| parent.fiber())
574 .and_then(|fiber| fiber.context())
575 .unwrap_or_else(|| inner.root.clone());
576 let fiber = parent_ctx.plugin(handle, config);
577 let Some(uid) = fiber.uid() else {
578 return Err(LoaderError::Cordis(
585 fiber
586 .error()
587 .unwrap_or_else(|| CordisError::new(ErrorCode::InactiveEffect)),
588 ));
589 };
590 entry.set_fiber(Some(fiber.clone()));
591 lock(&inner.state).entries.insert(uid, entry.clone());
592 emit(
593 inner,
594 crate::events::ENTRY_INIT,
595 vec![Value::new(entry.clone())],
596 );
597 Ok(())
598}
599
600fn stop_entry(inner: &LoaderInner, entry: &Entry) -> Result<()> {
603 for child in entry.children() {
604 stop_entry(inner, &child)?;
605 }
606 let Some(fiber) = entry.fiber() else {
607 return Ok(());
608 };
609 entry.set_fiber(None);
610 if let Some(uid) = fiber.uid() {
611 lock(&inner.state).entries.remove(&uid);
612 }
613 let _guard = OperatingGuard::new(&inner.state);
614 fiber.dispose().map_err(LoaderError::Cordis)
615}
616
617fn patch_entry(inner: &LoaderInner, entry: &Entry) -> Result<()> {
622 if !entry.enabled() {
623 return stop_entry(inner, entry);
624 }
625 let Some(fiber) = entry.fiber() else {
626 return Ok(());
627 };
628 let new_config = entry.resolved_config()?.unwrap_or(Node::Null);
629 let current = fiber
630 .config()
631 .downcast::<Node>()
632 .ok()
633 .map(|node| (*node).clone());
634 if current.as_ref() != Some(&new_config) {
635 emit(
636 inner,
637 crate::events::BEFORE_PATCH,
638 vec![Value::new(entry.clone())],
639 );
640 if let Err(error) = fiber.update_value(Config::new(new_config)) {
641 if let Some(old_config) = current {
648 let mut options = entry_options_with_children(entry);
649 options.config = Some(old_config);
650 if let Err(revert) = inner.tree.update_entry(&entry.path(), options, None, None) {
651 lock(&inner.state).last_error = Some(format!(
652 "failed to roll back config of {}: {revert}",
653 entry.path()
654 ));
655 }
656 }
657 return Err(LoaderError::Cordis(error));
658 }
659 emit(
660 inner,
661 crate::events::AFTER_PATCH,
662 vec![Value::new(entry.clone())],
663 );
664 }
665 Ok(())
666}
667
668fn start_subtree(inner: &LoaderInner, entry: &Entry) -> Result<()> {
671 start_entry(inner, entry)?;
672 for child in entry.children() {
673 start_subtree(inner, &child)?;
674 }
675 Ok(())
676}
677
678fn entry_depth(entry: &Entry) -> usize {
681 let mut depth = 0;
682 let mut current = entry.clone();
683 while let Some(parent) = current.parent() {
684 depth += 1;
685 current = parent;
686 }
687 depth
688}
689
690fn entry_options_with_children(entry: &Entry) -> EntryOptions {
693 let mut options = entry.options();
694 options.group = entry
695 .children()
696 .iter()
697 .map(entry_options_with_children)
698 .collect();
699 options
700}
701
702fn write_back(inner: &LoaderInner) -> Result<()> {
706 let mut jobs: Vec<(LoaderFile, Vec<EntryOptions>)> = vec![(
707 inner.file.clone(),
708 inner
709 .tree
710 .top_level()
711 .iter()
712 .map(to_stripped_options)
713 .collect(),
714 )];
715 for entry in inner.tree.entries() {
716 if entry.options().import_url().is_some() {
717 if let Some(file) = lock(&inner.imports).get(&import_canonical(inner, &entry)) {
718 let children = entry.children().iter().map(to_stripped_options).collect();
719 jobs.push((file.clone(), children));
720 }
721 }
722 }
723 let debounce = *lock(&inner.write_debounce);
724 for (file, entries) in jobs {
725 let mut document = file.read()?;
726 document.entries = entries;
727 match debounce {
728 Some(delay) => file.write_deferred(document, delay),
729 None => file.write(&document)?,
730 }
731 }
732 Ok(())
733}
734
735fn to_stripped_options(entry: &Entry) -> EntryOptions {
739 fn strip(options: &mut EntryOptions) {
740 if options.import_url().is_some() {
741 options.group.clear();
743 return;
744 }
745 options.group.retain(|child| child.import_url().is_none());
746 for child in &mut options.group {
747 strip(child);
748 }
749 }
750 let mut options = entry_options_with_children(entry);
751 strip(&mut options);
752 options
753}
754
755fn import_path(base_file: &LoaderFile, url: &str) -> PathBuf {
758 let direct = Path::new(url);
759 if direct.is_absolute() {
760 return direct.to_path_buf();
761 }
762 match base_file.path().parent() {
763 Some(parent) => parent.join(url),
764 None => direct.to_path_buf(),
765 }
766}
767
768fn import_canonical(inner: &LoaderInner, entry: &Entry) -> PathBuf {
770 let url = entry.options().import_url().unwrap_or_default().to_owned();
771 let path = import_path(&inner.file, &url);
772 std::fs::canonicalize(&path).unwrap_or(path)
773}
774
775fn compose(
787 file: &LoaderFile,
788 imports: &mut HashMap<PathBuf, LoaderFile>,
789 active: &mut HashSet<PathBuf>,
790 mounted: &mut HashSet<PathBuf>,
791 errors: &mut Vec<String>,
792) -> cordis_include::Result<(Vec<EntryOptions>, bool)> {
793 let document = file.read()?;
797 let mut entries = Vec::with_capacity(document.entries.len());
798 let mut dirty = document.entries.iter().any(|options| options.id.is_none());
799 for mut options in document.entries {
800 if let Some(url) = options.import_url().map(str::to_owned) {
801 let path = import_path(file, &url);
802 let canonical = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
803 if !active.insert(canonical.clone()) {
804 errors.push(format!("import cycle detected at {}", path.display()));
805 continue;
808 }
809 if !mounted.insert(canonical.clone()) {
810 errors.push(format!(
811 "duplicate import: {} is already mounted elsewhere; \
812 the import graph must be a tree",
813 path.display()
814 ));
815 active.remove(&canonical);
816 continue;
817 }
818 match LoaderFile::open(&path) {
819 Ok(sub_file) => {
820 match compose(&sub_file, imports, active, mounted, errors) {
821 Ok((sub_entries, sub_dirty)) => {
822 dirty |= sub_dirty;
823 options.group = sub_entries;
824 }
825 Err(error) => {
826 errors.push(format!(
827 "cannot read import {}: {error}",
828 sub_file.path().display()
829 ));
830 options.group.clear();
833 }
834 }
835 imports.insert(canonical.clone(), sub_file);
836 }
837 Err(error) => errors.push(format!(
838 "cannot open import {} ({}: {error})",
839 path.display(),
840 file.path().display()
841 )),
842 }
843 active.remove(&canonical);
846 }
847 entries.push(options);
848 }
849 Ok((entries, dirty))
850}
851
852fn handle_status(inner: &Arc<LoaderInner>, event: &cordis::Event) -> cordis::EventResult {
864 let Some(fiber) = event.arg::<Fiber>(0).ok().flatten() else {
865 return Ok(None);
866 };
867 if fiber.state() != FiberState::Disposed {
868 return Ok(None);
869 }
870 if lock(&inner.state).operating > 0 {
871 return Ok(None);
872 }
873 let Some(entry) = lock(&inner.state)
874 .entries
875 .values()
876 .find(|entry| entry.fiber().is_some_and(|started| started.ptr_eq(&fiber)))
877 .cloned()
878 else {
879 return Ok(None);
880 };
881 let deferred = std::thread::Builder::new()
882 .name("cordis-self-dispose".to_owned())
883 .spawn({
884 let inner = Arc::clone(inner);
887 let entry = entry.clone();
888 move || {
889 let _operation = inner.operation.guard();
892 if let Err(error) = persist_self_dispose(&inner, &entry) {
893 lock(&inner.state).last_error = Some(error.to_string());
894 }
895 }
896 });
897 match deferred {
898 Ok(_join) => {}
899 Err(_) => {
900 let _operation = inner.operation.guard();
903 if let Err(error) = persist_self_dispose(inner, &entry) {
904 lock(&inner.state).last_error = Some(error.to_string());
905 }
906 }
907 }
908 Ok(None)
909}
910
911fn persist_self_dispose(inner: &LoaderInner, entry: &Entry) -> Result<()> {
913 {
914 let mut state = lock(&inner.state);
915 let key = state
916 .entries
917 .iter()
918 .find(|(_, mapped)| Entry::ptr_eq(mapped, entry))
919 .map(|(uid, _)| *uid);
920 if let Some(uid) = key {
921 state.entries.remove(&uid);
922 }
923 }
924 entry.set_fiber(None);
925 let mut options = entry_options_with_children(entry);
926 options.disabled = true;
927 inner
928 .tree
929 .update_entry(&entry.path(), options, None, None)?;
930 write_back(inner)?;
931 emit(
932 inner,
933 crate::events::PARTIAL_DISPOSE,
934 vec![Value::new(entry.clone())],
935 );
936 Ok(())
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use cordis::{Inject, PluginOutput, plugin_sync};
943
944 #[test]
951 fn rejected_start_leaves_the_entry_retryable() {
952 let path = std::env::temp_dir().join(format!(
953 "cordis-loader-rejected-start-{}-{}.yml",
954 std::process::id(),
955 std::time::SystemTime::now()
956 .duration_since(std::time::UNIX_EPOCH)
957 .map(|elapsed| elapsed.as_nanos() as u64)
958 .unwrap_or(0)
959 ));
960 let _ = std::fs::remove_file(&path);
961 let mut registry = PluginRegistry::new();
962 registry.register("worker", || {
963 plugin_sync::<Node, _>("worker", Inject::default(), |_, _| Ok(PluginOutput::none()))
964 });
965 let root = Context::new();
966 let loader = Loader::open(
967 &root,
968 LoaderConfig::new(&path)
969 .with_registry(registry)
970 .with_initial(cordis_include::Document::with_entries(vec![
971 EntryOptions::new("group")
972 .with_id("g1")
973 .with_group(vec![EntryOptions::new("worker").with_id("c1")]),
974 ])),
975 )
976 .unwrap();
977 let inner = &loader.inner;
978 let group = inner.tree.resolve("g1").unwrap();
979 let child = inner.tree.resolve("g1:c1").unwrap();
980 assert!(group.fiber().is_some() && child.fiber().is_some());
981
982 {
985 let _operating = OperatingGuard::new(&inner.state);
986 group.fiber().unwrap().dispose().unwrap();
987 }
988 child.set_fiber(None);
989
990 let result = start_entry(inner, &child);
991 assert!(result.is_err(), "the registry rejection must surface");
992 assert!(child.fiber().is_none(), "no rejected fiber recorded");
993
994 assert!(start_entry(inner, &child).is_err());
997 assert!(child.fiber().is_none());
998
999 drop(loader);
1000 let _ = std::fs::remove_file(&path);
1001 }
1002}