use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use day_spec::present::{PresentResult, PresentSpec};
use crate::with_tree;
type LocalFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
day_reactive::tls_slots! {
present;
static TASKS: RefCell<HashMap<u64, Option<LocalFuture>>> = RefCell::new(HashMap::new());
static NEXT_TASK: Cell<u64> = const { Cell::new(1) };
static SHUTTING_DOWN: Cell<bool> = const { Cell::new(false) };
static REWAKE: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
static PENDING: RefCell<HashMap<u64, PendingEntry>> = RefCell::new(HashMap::new());
static NEXT_REQ: Cell<u64> = const { Cell::new(1) };
static PRESENTATION_MODE: Cell<Option<PresentationMode>> = const { Cell::new(None) };
}
pub use day_spec::present::app_temp_dir;
pub fn sleep(ms: u32) -> Sleep {
Sleep {
ms,
scheduled: false,
shared: std::sync::Arc::new(SleepShared {
done: std::sync::atomic::AtomicBool::new(false),
waker: std::sync::Mutex::new(None),
}),
}
}
struct SleepShared {
done: std::sync::atomic::AtomicBool,
waker: std::sync::Mutex<Option<Waker>>,
}
pub struct Sleep {
ms: u32,
scheduled: bool,
shared: std::sync::Arc<SleepShared>,
}
impl Future for Sleep {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
use std::sync::atomic::Ordering;
if self.shared.done.load(Ordering::Acquire) {
return Poll::Ready(());
}
if let Ok(mut w) = self.shared.waker.lock() {
*w = Some(cx.waker().clone());
}
if !self.scheduled {
self.scheduled = true;
let shared = self.shared.clone();
day_reactive::on_main_delayed(self.ms, move || {
shared.done.store(true, Ordering::Release);
if let Ok(mut w) = shared.waker.lock()
&& let Some(w) = w.take()
{
w.wake();
}
});
}
Poll::Pending
}
}
struct PendingEntry {
shared: Rc<PendingShared>,
spec: PresentSpec,
native: bool,
}
struct PendingShared {
result: RefCell<Option<PresentResult>>,
waker: RefCell<Option<Waker>>,
}
#[derive(Clone, Copy)]
pub struct TaskHandle {
id: u64,
_not_send: std::marker::PhantomData<*const ()>,
}
impl TaskHandle {
pub fn abort(self) {
let fut = TASKS.try_with(|t| t.borrow_mut().remove(&self.id));
drop(fut);
}
pub fn is_finished(self) -> bool {
!TASKS
.try_with(|t| t.borrow().contains_key(&self.id))
.unwrap_or(false)
}
}
pub fn task(fut: impl Future<Output = ()> + 'static) -> TaskHandle {
if SHUTTING_DOWN.try_with(Cell::get).unwrap_or(true) {
return TaskHandle {
id: 0,
_not_send: std::marker::PhantomData,
};
}
let id = NEXT_TASK.with(|c| {
let v = c.get();
c.set(v + 1);
v
});
TASKS.with(|t| t.borrow_mut().insert(id, Some(Box::pin(fut))));
poll_task(id);
TaskHandle {
id,
_not_send: std::marker::PhantomData,
}
}
pub(crate) fn shutdown_tasks() {
if SHUTTING_DOWN
.try_with(|closing| closing.replace(true))
.unwrap_or(true)
{
return;
}
let tasks = TASKS.with(|tasks| std::mem::take(&mut *tasks.borrow_mut()));
REWAKE.with(|wake| wake.borrow_mut().clear());
for future in tasks.into_values().flatten() {
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(future))).is_err() {
log::warn!("an async task panicked during shutdown; continuing task cleanup");
day_reactive::recover_from_panic();
crate::notify_contained_panic();
}
}
}
fn poll_task(id: u64) {
if SHUTTING_DOWN.try_with(Cell::get).unwrap_or(true) {
return;
}
let fut = TASKS.with(|t| t.borrow_mut().get_mut(&id).and_then(|s| s.take()));
let Some(mut fut) = fut else {
let live = TASKS.with(|t| t.borrow().contains_key(&id));
if live {
REWAKE.with(|r| r.borrow_mut().push(id));
}
return;
};
let waker = task_waker(id);
let mut cx = Context::from_waker(&waker);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(()) => {
TASKS.with(|t| {
t.borrow_mut().remove(&id);
});
REWAKE.with(|r| r.borrow_mut().retain(|w| *w != id));
return;
}
Poll::Pending => {
let again = REWAKE.with(|r| {
let mut r = r.borrow_mut();
let had = r.contains(&id);
r.retain(|w| *w != id);
had
});
if !again {
TASKS.with(|t| {
if let Some(slot) = t.borrow_mut().get_mut(&id) {
*slot = Some(fut);
}
});
return;
}
}
}
}
}
fn task_waker(id: u64) -> Waker {
unsafe { Waker::from_raw(raw_waker(id as usize as *const ())) }
}
fn raw_waker(data: *const ()) -> RawWaker {
RawWaker::new(data, &VTABLE)
}
static VTABLE: RawWakerVTable = RawWakerVTable::new(
raw_waker, |d| wake_task(d as usize as u64),
|d| wake_task(d as usize as u64),
|_| {},
);
fn wake_task(id: u64) {
day_reactive::on_main(move || poll_task(id));
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PresentationMode {
Native,
Scripted,
}
pub fn presentation_mode() -> PresentationMode {
PRESENTATION_MODE.with(|slot| {
if let Some(mode) = slot.get() {
return mode;
}
let mode = if std::env::var("DAY_TEST_DIALOGS").as_deref() == Ok("scripted") {
PresentationMode::Scripted
} else {
PresentationMode::Native
};
slot.set(Some(mode));
mode
})
}
pub fn init_presentation_mode(mode: PresentationMode) {
PRESENTATION_MODE.with(|slot| {
if slot.get().is_none() {
slot.set(Some(mode));
}
});
}
pub fn set_presentation_mode(mode: PresentationMode) -> Result<(), &'static str> {
if mode != presentation_mode() && PENDING.with(|p| !p.borrow().is_empty()) {
return Err("answer pending dialogs with respond before changing dialog_mode");
}
PRESENTATION_MODE.with(|slot| slot.set(Some(mode)));
Ok(())
}
pub fn present(spec: PresentSpec) -> PresentFuture {
let req = NEXT_REQ.with(|c| {
let v = c.get();
c.set(v + 1);
v
});
PresentFuture {
req,
spec: Some(spec),
shared: Rc::new(PendingShared {
result: RefCell::new(None),
waker: RefCell::new(None),
}),
presented: false,
}
}
pub struct PresentFuture {
req: u64,
spec: Option<PresentSpec>,
shared: Rc<PendingShared>,
presented: bool,
}
impl Future for PresentFuture {
type Output = PresentResult;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<PresentResult> {
if let Some(result) = self.shared.result.borrow_mut().take() {
return Poll::Ready(result);
}
if !self.presented {
self.presented = true;
let req = self.req;
let spec = self.spec.take().expect("present spec");
let native = presentation_mode() == PresentationMode::Native;
PENDING.with(|p| {
p.borrow_mut().insert(
req,
PendingEntry {
shared: self.shared.clone(),
spec: spec.clone(),
native,
},
)
});
if native {
with_tree(|t| t.present(req, &spec));
}
}
*self.shared.waker.borrow_mut() = Some(cx.waker().clone());
Poll::Pending
}
}
pub fn resolve_presentation(req: u64, result: PresentResult) {
let entry = PENDING.with(|p| p.borrow_mut().remove(&req));
if let Some(entry) = entry {
*entry.shared.result.borrow_mut() = Some(result);
if let Some(waker) = entry.shared.waker.borrow_mut().take() {
waker.wake();
}
}
}
pub fn respond_presentation(req: u64, result: PresentResult) -> bool {
let native = PENDING.with(|p| p.borrow().get(&req).map(|entry| entry.native));
let Some(native) = native else {
return false;
};
resolve_presentation(req, result);
if native {
with_tree(|t| t.dismiss(req));
}
true
}
pub fn pending_presentation() -> Option<(u64, PresentSpec)> {
PENDING.with(|p| {
p.borrow()
.iter()
.max_by_key(|(req, _)| **req)
.map(|(req, entry)| (*req, entry.spec.clone()))
})
}
#[cfg(test)]
mod task_tests {
use super::*;
use std::cell::Cell;
use std::task::Waker;
#[test]
fn scripted_dialogs_preserve_answers_without_calling_the_toolkit() {
use day_spec::present::{ButtonRole, PresentButton};
let (toolkit, probe) = day_mock::MockToolkit::new();
crate::tree::install_tree(Box::new(crate::Tree::new(
toolkit,
day_mock::MockHandle(0),
day_spec::Size::new(100.0, 100.0),
)));
let cases = [
(
PresentSpec::Dialog {
title: "confirm".into(),
message: None,
buttons: vec![PresentButton {
label: "Yes".into(),
role: ButtonRole::Default,
}],
sheet: false,
},
PresentResult::Button(0),
),
(
PresentSpec::Prompt {
title: "prompt".into(),
message: None,
placeholder: String::new(),
initial: String::new(),
ok: "OK".into(),
cancel: "Cancel".into(),
},
PresentResult::Text("Ada".into()),
),
(
PresentSpec::OpenFile {
title: "open".into(),
filters: vec![],
multiple: false,
},
PresentResult::Files(vec!["fixture.txt".into()]),
),
(
PresentSpec::OpenFile {
title: "open many".into(),
filters: vec![],
multiple: true,
},
PresentResult::Files(vec!["a.txt".into(), "b.txt".into()]),
),
(
PresentSpec::OpenFolder {
title: "folder".into(),
},
PresentResult::Files(vec!["music".into()]),
),
(
PresentSpec::SaveFile {
title: "save".into(),
suggested_name: "copy.txt".into(),
src_path: "staged.txt".into(),
filters: vec![],
},
PresentResult::Files(vec!["copy.txt".into()]),
),
];
let mut cx = Context::from_waker(Waker::noop());
for mode in [PresentationMode::Native, PresentationMode::Scripted] {
set_presentation_mode(mode).unwrap();
init_presentation_mode(PresentationMode::Native);
assert_eq!(presentation_mode(), mode);
for (spec, answer) in &cases {
for result in [answer.clone(), PresentResult::Dismissed] {
probe.clear_log();
let mut future = Box::pin(present(spec.clone()));
assert!(future.as_mut().poll(&mut cx).is_pending());
let (req, pending) = pending_presentation().unwrap();
assert_eq!(&pending, spec);
let other = if mode == PresentationMode::Native {
PresentationMode::Scripted
} else {
PresentationMode::Native
};
assert!(set_presentation_mode(other).is_err());
assert_eq!(presentation_mode(), mode);
assert!(respond_presentation(req, result.clone()));
resolve_presentation(req, PresentResult::Dismissed);
assert!(!respond_presentation(req, PresentResult::Dismissed));
assert_eq!(future.as_mut().poll(&mut cx), Poll::Ready(result));
assert!(pending_presentation().is_none());
let log = probe.log();
if mode == PresentationMode::Native {
assert_eq!(log.len(), 2, "{log:?}");
assert!(log[0].starts_with("present req="));
assert!(log[1].starts_with("dismiss req="));
} else {
assert!(
log.is_empty(),
"scripted requests reached the toolkit: {log:?}"
);
}
}
}
}
set_presentation_mode(PresentationMode::Native).unwrap();
crate::uninstall_tree();
}
fn init() {
day_reactive::install_main_poster(|f| f());
}
struct DropFlag(Rc<Cell<bool>>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.set(true);
}
}
fn pending_forever(flag: Rc<Cell<bool>>) -> impl Future<Output = ()> {
let guard = DropFlag(flag);
std::future::poll_fn(move |_| {
let _ = &guard;
Poll::Pending
})
}
#[test]
fn task_runs_to_completion() {
init();
let ran = Rc::new(Cell::new(false));
let r = ran.clone();
let h = task(async move { r.set(true) });
assert!(ran.get());
assert!(h.is_finished());
}
#[test]
fn abort_drops_pending_future() {
init();
let dropped = Rc::new(Cell::new(false));
let h = task(pending_forever(dropped.clone()));
assert!(!h.is_finished());
assert!(!dropped.get());
h.abort();
assert!(dropped.get());
assert!(h.is_finished());
}
#[test]
fn abort_after_completion_is_noop() {
init();
let h = task(async {});
assert!(h.is_finished());
h.abort();
assert!(h.is_finished());
}
#[test]
fn abort_self_while_polling() {
init();
let handle: Rc<Cell<Option<TaskHandle>>> = Rc::new(Cell::new(None));
let dropped = Rc::new(Cell::new(false));
let waker_slot: Rc<RefCell<Option<Waker>>> = Rc::new(RefCell::new(None));
let h_cell = handle.clone();
let w_slot = waker_slot.clone();
let guard = DropFlag(dropped.clone());
let h = task(std::future::poll_fn(move |cx| {
let _ = &guard;
match h_cell.get() {
None => {
*w_slot.borrow_mut() = Some(cx.waker().clone());
Poll::Pending
}
Some(h) => {
h.abort();
Poll::Pending
}
}
}));
handle.set(Some(h));
assert!(!dropped.get());
let waker = waker_slot
.borrow_mut()
.take()
.expect("waker from first poll");
waker.wake(); assert!(dropped.get());
assert!(h.is_finished());
}
#[test]
fn abort_reentrancy_from_drop() {
init();
struct SpawnOnDrop;
impl Drop for SpawnOnDrop {
fn drop(&mut self) {
task(async {});
}
}
let guard = SpawnOnDrop;
let h = task(std::future::poll_fn(move |_| {
let _ = &guard;
Poll::<()>::Pending
}));
h.abort();
assert!(h.is_finished());
}
#[test]
fn termination_drops_tasks_before_reactive_tls_and_dependency_shutdown() {
const CHILD: &str = "DAY_TASK_SHUTDOWN_CHILD";
if std::env::var_os(CHILD).is_some() {
std::thread::spawn(|| {
init();
assert!(!crate::has_tree()); struct Updating(day_reactive::Signal<bool>);
impl Drop for Updating {
fn drop(&mut self) {
self.0.set(false);
}
}
let active = day_reactive::Signal::new(true);
let guard = Updating(active);
let handle = task(std::future::poll_fn(move |_| {
let _ = &guard;
Poll::Pending
}));
crate::on_lifecycle(day_spec::Lifecycle::WillTerminate, move || {
assert!(!active.get_untracked());
assert!(handle.is_finished());
});
crate::dispatch_lifecycle(day_spec::Lifecycle::WillTerminate);
crate::dispatch_lifecycle(day_spec::Lifecycle::WillTerminate);
assert!(!active.get_untracked());
})
.join()
.unwrap();
return;
}
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"present::task_tests::termination_drops_tasks_before_reactive_tls_and_dependency_shutdown",
"--nocapture",
])
.env(CHILD, "1")
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("1 passed"));
}
#[test]
fn shutdown_rejects_reentrant_tasks_and_late_wakes() {
init();
struct SpawnOnDrop(Rc<Cell<bool>>);
impl Drop for SpawnOnDrop {
fn drop(&mut self) {
let polled = self.0.clone();
let h = task(async move { polled.set(true) });
assert!(h.is_finished());
h.abort();
}
}
let polled = Rc::new(Cell::new(false));
let guard = SpawnOnDrop(polled.clone());
let wake = Rc::new(RefCell::new(None));
let saved = wake.clone();
let polls = Rc::new(Cell::new(0));
let count = polls.clone();
let h = task(std::future::poll_fn(move |cx| {
let _ = &guard;
count.set(count.get() + 1);
*saved.borrow_mut() = Some(cx.waker().clone());
Poll::Pending
}));
shutdown_tasks();
wake.borrow_mut().take().unwrap().wake();
shutdown_tasks();
assert!(h.is_finished());
assert!(!polled.get());
assert_eq!(polls.get(), 1);
assert!(TASKS.with(|tasks| tasks.borrow().is_empty()));
}
#[test]
fn shutdown_continues_after_a_task_destructor_panics() {
init();
struct Panics;
impl Drop for Panics {
fn drop(&mut self) {
panic!("synthetic task destructor failure");
}
}
let guard = Panics;
task(std::future::poll_fn(move |_| {
let _ = &guard;
Poll::Pending
}));
let dropped = Rc::new(Cell::new(false));
let handle = task(pending_forever(dropped.clone()));
shutdown_tasks();
assert!(dropped.get());
assert!(handle.is_finished());
assert!(TASKS.with(|tasks| tasks.borrow().is_empty()));
}
}