Skip to main content

day_core/
present.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! Imperative presentation (docs/dialogs.md): a minimal single-threaded async executor
5//! (`task`) plus the pending-request registry that routes a `present(spec).await` through
6//! the tree to the backend and its `Event::PresentResult` answer back to the future.
7//!
8//! Everything is thread-local and `!Send`: Day has one UI thread. The executor is std-only
9//! (no async runtime): tasks are boxed futures polled on the main loop; a presentation
10//! future parks a `Waker` that re-polls its task through `day_reactive::on_main`.
11
12use std::cell::{Cell, RefCell};
13use std::collections::HashMap;
14use std::future::Future;
15use std::pin::Pin;
16use std::rc::Rc;
17use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
18
19use day_spec::present::{PresentResult, PresentSpec};
20
21use crate::with_tree;
22
23type LocalFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
24
25day_reactive::tls_slots! {
26    present;
27    /// Live async flows. `None` = currently being polled (taken out to avoid re-entrant borrow).
28    static TASKS: RefCell<HashMap<u64, Option<LocalFuture>>> = RefCell::new(HashMap::new());
29    static NEXT_TASK: Cell<u64> = const { Cell::new(1) };
30    static SHUTTING_DOWN: Cell<bool> = const { Cell::new(false) };
31    /// Tasks woken while their own poll was running (their slot was `None`): the wake would
32    /// otherwise land on a missing future and be lost, so `poll_task` polls them again.
33    static REWAKE: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
34    static PENDING: RefCell<HashMap<u64, PendingEntry>> = RefCell::new(HashMap::new());
35    static NEXT_REQ: Cell<u64> = const { Cell::new(1) };
36    static PRESENTATION_MODE: Cell<Option<PresentationMode>> = const { Cell::new(None) };
37}
38
39/// An app-writable scratch directory (docs/files.md), re-exported from `day_spec::present` so
40/// `day_core::app_temp_dir()` keeps working for the pieces layer's file-save staging.
41pub use day_spec::present::app_temp_dir;
42
43/// Resolve after (at least) `ms` milliseconds, on the UI thread: the portable delay for
44/// `day::task` flows (docs/async.md). Rides `Platform::post_delayed`, so it works on
45/// single-threaded hosts (web) where `std::thread::sleep` + `Setter` cannot.
46pub fn sleep(ms: u32) -> Sleep {
47    Sleep {
48        ms,
49        scheduled: false,
50        shared: std::sync::Arc::new(SleepShared {
51            done: std::sync::atomic::AtomicBool::new(false),
52            waker: std::sync::Mutex::new(None),
53        }),
54    }
55}
56
57struct SleepShared {
58    done: std::sync::atomic::AtomicBool,
59    waker: std::sync::Mutex<Option<Waker>>,
60}
61
62/// Future returned by [`sleep`].
63pub struct Sleep {
64    ms: u32,
65    scheduled: bool,
66    shared: std::sync::Arc<SleepShared>,
67}
68
69impl Future for Sleep {
70    type Output = ();
71
72    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
73        use std::sync::atomic::Ordering;
74        if self.shared.done.load(Ordering::Acquire) {
75            return Poll::Ready(());
76        }
77        if let Ok(mut w) = self.shared.waker.lock() {
78            *w = Some(cx.waker().clone());
79        }
80        if !self.scheduled {
81            self.scheduled = true;
82            let shared = self.shared.clone();
83            day_reactive::on_main_delayed(self.ms, move || {
84                shared.done.store(true, Ordering::Release);
85                if let Ok(mut w) = shared.waker.lock()
86                    && let Some(w) = w.take()
87                {
88                    w.wake();
89                }
90            });
91        }
92        Poll::Pending
93    }
94}
95
96struct PendingEntry {
97    shared: Rc<PendingShared>,
98    spec: PresentSpec,
99    native: bool,
100}
101
102struct PendingShared {
103    result: RefCell<Option<PresentResult>>,
104    waker: RefCell<Option<Waker>>,
105}
106
107// ---------------------------------------------------------------------------
108// Executor
109// ---------------------------------------------------------------------------
110
111/// A handle to a spawned [`task`]. `Copy` and `!Send` (the executor is thread-local).
112///
113/// Task ids are monotonic and never reused, so a stale handle is always a harmless miss:
114/// [`TaskHandle::abort`] after completion is a no-op and [`TaskHandle::is_finished`] stays true.
115#[derive(Clone, Copy)]
116pub struct TaskHandle {
117    id: u64,
118    _not_send: std::marker::PhantomData<*const ()>,
119}
120
121impl TaskHandle {
122    /// Remove and drop the task's future. An in-flight `.await` cancels via `Drop` (e.g. a
123    /// `FetchFuture` inside cancels its platform request). No-op if the task already finished.
124    pub fn abort(self) {
125        // Take the future out of the map and drop it after the RefCell borrow ends: a future
126        // whose Drop re-enters the executor (spawns a task, aborts another handle) would
127        // otherwise hit a live borrow. If the task is mid-poll its slot was taken (`None`);
128        // removing the entry then makes `poll_task`'s Pending put-back find nothing, and the
129        // future drops there instead.
130        let fut = TASKS.try_with(|t| t.borrow_mut().remove(&self.id));
131        drop(fut);
132    }
133
134    /// Whether the task no longer runs (completed or aborted).
135    pub fn is_finished(self) -> bool {
136        !TASKS
137            .try_with(|t| t.borrow().contains_key(&self.id))
138            .unwrap_or(false)
139    }
140}
141
142/// Spawn an async flow onto Day's main-loop executor. This is the opt-in entry point for actions
143/// that open modals or pickers: `button.action(|| day::task(async move { … .await … }))`.
144/// The future is polled once before this returns; the returned handle can [`TaskHandle::abort`]
145/// it and is freely discardable.
146/// At app termination pending tasks are dropped while reactive state is still available.
147/// Tasks submitted after shutdown begins are dropped without being polled.
148pub fn task(fut: impl Future<Output = ()> + 'static) -> TaskHandle {
149    if SHUTTING_DOWN.try_with(Cell::get).unwrap_or(true) {
150        return TaskHandle {
151            id: 0,
152            _not_send: std::marker::PhantomData,
153        };
154    }
155    let id = NEXT_TASK.with(|c| {
156        let v = c.get();
157        c.set(v + 1);
158        v
159    });
160    TASKS.with(|t| t.borrow_mut().insert(id, Some(Box::pin(fut))));
161    poll_task(id);
162    TaskHandle {
163        id,
164        _not_send: std::marker::PhantomData,
165    }
166}
167
168/// End async work before lifecycle handlers close its dependencies and before TLS teardown.
169/// Detach every task first: destructors may write signals, abort peers, or try to spawn work.
170pub(crate) fn shutdown_tasks() {
171    if SHUTTING_DOWN
172        .try_with(|closing| closing.replace(true))
173        .unwrap_or(true)
174    {
175        return;
176    }
177    let tasks = TASKS.with(|tasks| std::mem::take(&mut *tasks.borrow_mut()));
178    REWAKE.with(|wake| wake.borrow_mut().clear());
179    for future in tasks.into_values().flatten() {
180        if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(future))).is_err() {
181            log::warn!("an async task panicked during shutdown; continuing task cleanup");
182            day_reactive::recover_from_panic();
183            crate::notify_contained_panic();
184        }
185    }
186}
187
188fn poll_task(id: u64) {
189    if SHUTTING_DOWN.try_with(Cell::get).unwrap_or(true) {
190        return;
191    }
192    let fut = TASKS.with(|t| t.borrow_mut().get_mut(&id).and_then(|s| s.take()));
193    let Some(mut fut) = fut else {
194        // Finished, or woken while its own poll runs: a synchronous poster (the mock) runs a
195        // timer's wake inside the sleep's poll. Remember it, and the poll in progress goes
196        // round again rather than parking a future nobody will wake.
197        let live = TASKS.with(|t| t.borrow().contains_key(&id));
198        if live {
199            REWAKE.with(|r| r.borrow_mut().push(id));
200        }
201        return;
202    };
203    let waker = task_waker(id);
204    let mut cx = Context::from_waker(&waker);
205    loop {
206        match fut.as_mut().poll(&mut cx) {
207            Poll::Ready(()) => {
208                TASKS.with(|t| {
209                    t.borrow_mut().remove(&id);
210                });
211                REWAKE.with(|r| r.borrow_mut().retain(|w| *w != id));
212                return;
213            }
214            Poll::Pending => {
215                let again = REWAKE.with(|r| {
216                    let mut r = r.borrow_mut();
217                    let had = r.contains(&id);
218                    r.retain(|w| *w != id);
219                    had
220                });
221                if !again {
222                    TASKS.with(|t| {
223                        if let Some(slot) = t.borrow_mut().get_mut(&id) {
224                            *slot = Some(fut);
225                        }
226                    });
227                    return;
228                }
229            }
230        }
231    }
232}
233
234// Minimal RawWaker: the "data" pointer IS the task id; waking re-polls it on the main loop.
235fn task_waker(id: u64) -> Waker {
236    // SAFETY: the vtable only ever reads the pointer back as an integer id; nothing is
237    // dereferenced, so any bit pattern is sound.
238    unsafe { Waker::from_raw(raw_waker(id as usize as *const ())) }
239}
240
241fn raw_waker(data: *const ()) -> RawWaker {
242    RawWaker::new(data, &VTABLE)
243}
244
245static VTABLE: RawWakerVTable = RawWakerVTable::new(
246    raw_waker, // clone
247    |d| wake_task(d as usize as u64),
248    |d| wake_task(d as usize as u64),
249    |_| {},
250);
251
252fn wake_task(id: u64) {
253    day_reactive::on_main(move || poll_task(id));
254}
255
256// ---------------------------------------------------------------------------
257// Presentation
258// ---------------------------------------------------------------------------
259
260/// Whether Day presents native controls or holds requests for an explicit script response.
261/// Scripted mode does not synthesize answers or affect OS permissions/external applications.
262#[derive(Clone, Copy, Debug, PartialEq, Eq)]
263pub enum PresentationMode {
264    Native,
265    Scripted,
266}
267
268/// The current UI-thread presentation policy. Ordinary app launches default to native.
269pub fn presentation_mode() -> PresentationMode {
270    PRESENTATION_MODE.with(|slot| {
271        if let Some(mode) = slot.get() {
272            return mode;
273        }
274        let mode = if std::env::var("DAY_TEST_DIALOGS").as_deref() == Ok("scripted") {
275            PresentationMode::Scripted
276        } else {
277            PresentationMode::Native
278        };
279        slot.set(Some(mode));
280        mode
281    })
282}
283
284/// Initialize browser hosts, which have no process environment, before constructing the UI.
285/// Further windows must not reset a policy selected by a running script.
286pub fn init_presentation_mode(mode: PresentationMode) {
287    PRESENTATION_MODE.with(|slot| {
288        if slot.get().is_none() {
289            slot.set(Some(mode));
290        }
291    });
292}
293
294/// Change policy for subsequent presentations. Answer existing requests first: switching
295/// cannot make an already-open system picker disappear.
296pub fn set_presentation_mode(mode: PresentationMode) -> Result<(), &'static str> {
297    if mode != presentation_mode() && PENDING.with(|p| !p.borrow().is_empty()) {
298        return Err("answer pending dialogs with respond before changing dialog_mode");
299    }
300    PRESENTATION_MODE.with(|slot| slot.set(Some(mode)));
301    Ok(())
302}
303
304/// Present a modal request under the current policy and await its answer (docs/dialogs.md).
305/// The pieces layer wraps
306/// this in `Alert`/`confirm`/`prompt`; call it directly for a custom `PresentSpec`.
307pub fn present(spec: PresentSpec) -> PresentFuture {
308    let req = NEXT_REQ.with(|c| {
309        let v = c.get();
310        c.set(v + 1);
311        v
312    });
313    PresentFuture {
314        req,
315        spec: Some(spec),
316        shared: Rc::new(PendingShared {
317            result: RefCell::new(None),
318            waker: RefCell::new(None),
319        }),
320        presented: false,
321    }
322}
323
324pub struct PresentFuture {
325    req: u64,
326    spec: Option<PresentSpec>,
327    shared: Rc<PendingShared>,
328    presented: bool,
329}
330
331impl Future for PresentFuture {
332    type Output = PresentResult;
333
334    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<PresentResult> {
335        if let Some(result) = self.shared.result.borrow_mut().take() {
336            return Poll::Ready(result);
337        }
338        if !self.presented {
339            self.presented = true;
340            let req = self.req;
341            let spec = self.spec.take().expect("present spec");
342            let native = presentation_mode() == PresentationMode::Native;
343            PENDING.with(|p| {
344                p.borrow_mut().insert(
345                    req,
346                    PendingEntry {
347                        shared: self.shared.clone(),
348                        spec: spec.clone(),
349                        native,
350                    },
351                )
352            });
353            if native {
354                with_tree(|t| t.present(req, &spec));
355            }
356        }
357        *self.shared.waker.borrow_mut() = Some(cx.waker().clone());
358        Poll::Pending
359    }
360}
361
362/// Deliver a native answer (the modal already dismissed itself). Called from `pump_events`
363/// on `Event::PresentResult`.
364pub fn resolve_presentation(req: u64, result: PresentResult) {
365    let entry = PENDING.with(|p| p.borrow_mut().remove(&req));
366    if let Some(entry) = entry {
367        *entry.shared.result.borrow_mut() = Some(result);
368        if let Some(waker) = entry.shared.waker.borrow_mut().take() {
369            waker.wake();
370        }
371    }
372}
373
374/// Answer a pending modal programmatically (dayscript). Resolves with the given result
375/// first (removing the pending request), then dismisses the control only if it was presented
376/// natively. Scripted requests never enter the toolkit. The native
377/// dismissal's own completion event finds nothing pending and is a no-op. False = no such
378/// pending request.
379pub fn respond_presentation(req: u64, result: PresentResult) -> bool {
380    let native = PENDING.with(|p| p.borrow().get(&req).map(|entry| entry.native));
381    let Some(native) = native else {
382        return false;
383    };
384    resolve_presentation(req, result);
385    if native {
386        with_tree(|t| t.dismiss(req));
387    }
388    true
389}
390
391/// The most recently opened still-pending presentation, for dayscript inspection/response.
392pub fn pending_presentation() -> Option<(u64, PresentSpec)> {
393    PENDING.with(|p| {
394        p.borrow()
395            .iter()
396            .max_by_key(|(req, _)| **req)
397            .map(|(req, entry)| (*req, entry.spec.clone()))
398    })
399}
400
401// ---------------------------------------------------------------------------
402// Executor tests
403// ---------------------------------------------------------------------------
404
405#[cfg(test)]
406mod task_tests {
407    use super::*;
408    use std::cell::Cell;
409    use std::task::Waker;
410
411    #[test]
412    fn scripted_dialogs_preserve_answers_without_calling_the_toolkit() {
413        use day_spec::present::{ButtonRole, PresentButton};
414        let (toolkit, probe) = day_mock::MockToolkit::new();
415        crate::tree::install_tree(Box::new(crate::Tree::new(
416            toolkit,
417            day_mock::MockHandle(0),
418            day_spec::Size::new(100.0, 100.0),
419        )));
420        let cases = [
421            (
422                PresentSpec::Dialog {
423                    title: "confirm".into(),
424                    message: None,
425                    buttons: vec![PresentButton {
426                        label: "Yes".into(),
427                        role: ButtonRole::Default,
428                    }],
429                    sheet: false,
430                },
431                PresentResult::Button(0),
432            ),
433            (
434                PresentSpec::Prompt {
435                    title: "prompt".into(),
436                    message: None,
437                    placeholder: String::new(),
438                    initial: String::new(),
439                    ok: "OK".into(),
440                    cancel: "Cancel".into(),
441                },
442                PresentResult::Text("Ada".into()),
443            ),
444            (
445                PresentSpec::OpenFile {
446                    title: "open".into(),
447                    filters: vec![],
448                    multiple: false,
449                },
450                PresentResult::Files(vec!["fixture.txt".into()]),
451            ),
452            (
453                PresentSpec::OpenFile {
454                    title: "open many".into(),
455                    filters: vec![],
456                    multiple: true,
457                },
458                PresentResult::Files(vec!["a.txt".into(), "b.txt".into()]),
459            ),
460            (
461                PresentSpec::OpenFolder {
462                    title: "folder".into(),
463                },
464                PresentResult::Files(vec!["music".into()]),
465            ),
466            (
467                PresentSpec::SaveFile {
468                    title: "save".into(),
469                    suggested_name: "copy.txt".into(),
470                    src_path: "staged.txt".into(),
471                    filters: vec![],
472                },
473                PresentResult::Files(vec!["copy.txt".into()]),
474            ),
475        ];
476        let mut cx = Context::from_waker(Waker::noop());
477        for mode in [PresentationMode::Native, PresentationMode::Scripted] {
478            set_presentation_mode(mode).unwrap();
479            // Creating another browser window must not reset an in-script choice.
480            init_presentation_mode(PresentationMode::Native);
481            assert_eq!(presentation_mode(), mode);
482            for (spec, answer) in &cases {
483                for result in [answer.clone(), PresentResult::Dismissed] {
484                    probe.clear_log();
485                    let mut future = Box::pin(present(spec.clone()));
486                    assert!(future.as_mut().poll(&mut cx).is_pending());
487                    let (req, pending) = pending_presentation().unwrap();
488                    assert_eq!(&pending, spec);
489                    let other = if mode == PresentationMode::Native {
490                        PresentationMode::Scripted
491                    } else {
492                        PresentationMode::Native
493                    };
494                    assert!(set_presentation_mode(other).is_err());
495                    assert_eq!(presentation_mode(), mode);
496                    assert!(respond_presentation(req, result.clone()));
497                    // A late native callback or a second answer must not overwrite the result.
498                    resolve_presentation(req, PresentResult::Dismissed);
499                    assert!(!respond_presentation(req, PresentResult::Dismissed));
500                    assert_eq!(future.as_mut().poll(&mut cx), Poll::Ready(result));
501                    assert!(pending_presentation().is_none());
502                    let log = probe.log();
503                    if mode == PresentationMode::Native {
504                        assert_eq!(log.len(), 2, "{log:?}");
505                        assert!(log[0].starts_with("present req="));
506                        assert!(log[1].starts_with("dismiss req="));
507                    } else {
508                        assert!(
509                            log.is_empty(),
510                            "scripted requests reached the toolkit: {log:?}"
511                        );
512                    }
513                }
514            }
515        }
516        set_presentation_mode(PresentationMode::Native).unwrap();
517        crate::uninstall_tree();
518    }
519
520    /// Every executor test is single-threaded, so an INLINE poster (run the closure now) is
521    /// correct. `install_main_poster` is first-install-wins, so repeated `init` calls are fine;
522    /// `TASKS` is thread-local, so tests don't see each other's tasks.
523    fn init() {
524        day_reactive::install_main_poster(|f| f());
525    }
526
527    /// Sets its flag when dropped; observes whether an aborted future was destroyed.
528    struct DropFlag(Rc<Cell<bool>>);
529    impl Drop for DropFlag {
530        fn drop(&mut self) {
531            self.0.set(true);
532        }
533    }
534
535    fn pending_forever(flag: Rc<Cell<bool>>) -> impl Future<Output = ()> {
536        let guard = DropFlag(flag);
537        std::future::poll_fn(move |_| {
538            let _ = &guard;
539            Poll::Pending
540        })
541    }
542
543    #[test]
544    fn task_runs_to_completion() {
545        init();
546        let ran = Rc::new(Cell::new(false));
547        let r = ran.clone();
548        let h = task(async move { r.set(true) });
549        assert!(ran.get());
550        assert!(h.is_finished());
551    }
552
553    #[test]
554    fn abort_drops_pending_future() {
555        init();
556        let dropped = Rc::new(Cell::new(false));
557        let h = task(pending_forever(dropped.clone()));
558        assert!(!h.is_finished());
559        assert!(!dropped.get());
560        h.abort();
561        assert!(dropped.get());
562        assert!(h.is_finished());
563    }
564
565    #[test]
566    fn abort_after_completion_is_noop() {
567        init();
568        let h = task(async {});
569        assert!(h.is_finished());
570        h.abort();
571        assert!(h.is_finished());
572    }
573
574    /// A task that aborts itself from inside `poll`: the slot is already taken (`None`), abort
575    /// removes the map entry, and the `Pending` put-back finds nothing. The future must drop
576    /// exactly once, after the poll returns, with no `RefCell` re-borrow.
577    #[test]
578    fn abort_self_while_polling() {
579        init();
580        let handle: Rc<Cell<Option<TaskHandle>>> = Rc::new(Cell::new(None));
581        let dropped = Rc::new(Cell::new(false));
582        let waker_slot: Rc<RefCell<Option<Waker>>> = Rc::new(RefCell::new(None));
583
584        let h_cell = handle.clone();
585        let w_slot = waker_slot.clone();
586        let guard = DropFlag(dropped.clone());
587        let h = task(std::future::poll_fn(move |cx| {
588            let _ = &guard;
589            match h_cell.get() {
590                None => {
591                    *w_slot.borrow_mut() = Some(cx.waker().clone());
592                    Poll::Pending
593                }
594                Some(h) => {
595                    h.abort();
596                    Poll::Pending
597                }
598            }
599        }));
600        handle.set(Some(h));
601        assert!(!dropped.get());
602        let waker = waker_slot
603            .borrow_mut()
604            .take()
605            .expect("waker from first poll");
606        waker.wake(); // inline poster: re-polls now; the second poll self-aborts
607        assert!(dropped.get());
608        assert!(h.is_finished());
609    }
610
611    /// Aborting a future whose Drop re-enters the executor (spawns a new task) must not panic:
612    /// `abort` releases the `TASKS` borrow before the future is destroyed.
613    #[test]
614    fn abort_reentrancy_from_drop() {
615        init();
616        struct SpawnOnDrop;
617        impl Drop for SpawnOnDrop {
618            fn drop(&mut self) {
619                task(async {});
620            }
621        }
622        let guard = SpawnOnDrop;
623        let h = task(std::future::poll_fn(move |_| {
624            let _ = &guard;
625            Poll::<()>::Pending
626        }));
627        h.abort();
628        assert!(h.is_finished());
629    }
630
631    #[test]
632    fn termination_drops_tasks_before_reactive_tls_and_dependency_shutdown() {
633        // A subprocess makes a regression's fatal TLS-destructor panic a test failure.
634        const CHILD: &str = "DAY_TASK_SHUTDOWN_CHILD";
635        if std::env::var_os(CHILD).is_some() {
636            std::thread::spawn(|| {
637                init();
638                assert!(!crate::has_tree()); // Core TLS initializes before reactive TLS.
639                struct Updating(day_reactive::Signal<bool>);
640                impl Drop for Updating {
641                    fn drop(&mut self) {
642                        self.0.set(false);
643                    }
644                }
645                let active = day_reactive::Signal::new(true);
646                let guard = Updating(active);
647                let handle = task(std::future::poll_fn(move |_| {
648                    let _ = &guard;
649                    Poll::Pending
650                }));
651                crate::on_lifecycle(day_spec::Lifecycle::WillTerminate, move || {
652                    assert!(!active.get_untracked());
653                    assert!(handle.is_finished());
654                });
655                crate::dispatch_lifecycle(day_spec::Lifecycle::WillTerminate);
656                crate::dispatch_lifecycle(day_spec::Lifecycle::WillTerminate);
657                assert!(!active.get_untracked());
658            })
659            .join()
660            .unwrap();
661            return;
662        }
663        let output = std::process::Command::new(std::env::current_exe().unwrap())
664            .args([
665                "--exact",
666                "present::task_tests::termination_drops_tasks_before_reactive_tls_and_dependency_shutdown",
667                "--nocapture",
668            ])
669            .env(CHILD, "1")
670            .output()
671            .unwrap();
672        assert!(
673            output.status.success(),
674            "{}",
675            String::from_utf8_lossy(&output.stderr)
676        );
677        assert!(String::from_utf8_lossy(&output.stdout).contains("1 passed"));
678    }
679
680    #[test]
681    fn shutdown_rejects_reentrant_tasks_and_late_wakes() {
682        init();
683        struct SpawnOnDrop(Rc<Cell<bool>>);
684        impl Drop for SpawnOnDrop {
685            fn drop(&mut self) {
686                let polled = self.0.clone();
687                let h = task(async move { polled.set(true) });
688                assert!(h.is_finished());
689                h.abort();
690            }
691        }
692        let polled = Rc::new(Cell::new(false));
693        let guard = SpawnOnDrop(polled.clone());
694        let wake = Rc::new(RefCell::new(None));
695        let saved = wake.clone();
696        let polls = Rc::new(Cell::new(0));
697        let count = polls.clone();
698        let h = task(std::future::poll_fn(move |cx| {
699            let _ = &guard;
700            count.set(count.get() + 1);
701            *saved.borrow_mut() = Some(cx.waker().clone());
702            Poll::Pending
703        }));
704        shutdown_tasks();
705        wake.borrow_mut().take().unwrap().wake();
706        shutdown_tasks();
707        assert!(h.is_finished());
708        assert!(!polled.get());
709        assert_eq!(polls.get(), 1);
710        assert!(TASKS.with(|tasks| tasks.borrow().is_empty()));
711    }
712
713    #[test]
714    fn shutdown_continues_after_a_task_destructor_panics() {
715        init();
716        struct Panics;
717        impl Drop for Panics {
718            fn drop(&mut self) {
719                panic!("synthetic task destructor failure");
720            }
721        }
722        let guard = Panics;
723        task(std::future::poll_fn(move |_| {
724            let _ = &guard;
725            Poll::Pending
726        }));
727        let dropped = Rc::new(Cell::new(false));
728        let handle = task(pending_forever(dropped.clone()));
729        shutdown_tasks();
730        assert!(dropped.get());
731        assert!(handle.is_finished());
732        assert!(TASKS.with(|tasks| tasks.borrow().is_empty()));
733    }
734}