use crate::host_words::{
CLOCK_WEEKDAYS, EXIT_HANDLER, FILE_HANDLER_PREFIX, INTERRUPT_HANDLER, LISTEN_CLOCK, LISTEN_FRAME, SHARED_HANDLER,
TIMER_HANDLER_PREFIX,
};
use std::cell::{Cell, RefCell};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::SystemTime;
use std::time::{Duration, Instant};
use wasmtime::{AsContextMut, Func, Instance, Result, Store, Val};
pub const NEW_EMPTY: &str = "new_empty";
pub const NEW_INT: &str = "new_int";
const FILE_CHECK_PERIOD: Duration = Duration::from_millis(100);
const AWAIT_CHECK_PERIOD: Duration = Duration::from_millis(10);
const SHARED_CHECK_PERIOD: Duration = Duration::from_millis(10);
struct Timer {
handler: String,
repeat: Repeat,
due: Option<Instant>,
trigger: Trigger,
}
enum Trigger {
Always,
FileChange(PathBuf, Option<FileStamp>),
Ready(String, Box<dyn FnMut() -> bool>),
WindowShows {
ran: bool,
},
}
impl Trigger {
fn holds(&mut self, due: &mut Option<Instant>) -> bool {
match self {
Trigger::Always => true,
Trigger::FileChange(path, seen) => {
let current = stamp(path);
let changed = current != *seen;
*seen = current;
changed
}
Trigger::Ready(_, ready) => {
let holds = ready();
if holds {
*due = None;
}
holds
}
Trigger::WindowShows { ran } => {
let holds = crate::system_values::window_shows() || !*ran;
*ran = true;
if !holds {
*due = None;
}
holds
}
}
}
}
#[derive(Clone, Copy)]
enum Repeat {
Every(Duration),
Clock {
minute_of_day: i64,
weekdays: i64,
once: bool,
},
}
impl Repeat {
fn first(self, now: Instant) -> Instant {
match self {
Repeat::Every(period) => now + period,
Repeat::Clock { minute_of_day, weekdays, .. } => {
let (weekday, second_of_day) = local_time();
now + Duration::from_secs(seconds_until_on(minute_of_day, weekdays, weekday, second_of_day) as u64)
}
}
}
fn after(self, due: Instant, now: Instant) -> Option<Instant> {
match self {
Repeat::Every(period) => Some((due + period).max(now)),
Repeat::Clock { once: true, .. } => None,
Repeat::Clock { .. } => Some(self.first(now)),
}
}
fn spoken(self) -> String {
match self {
Repeat::Every(period) => format!("every {}", spoken(period)),
Repeat::Clock { minute_of_day, once: true, .. } => format!("at {}", clock_time(minute_of_day)),
Repeat::Clock { minute_of_day, weekdays, once: false } => {
format!("every {} at {}", spoken_days(weekdays), clock_time(minute_of_day))
}
}
}
}
const WEEKDAY_NAMES: [&str; 7] = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
pub const EVERY_DAY: i64 = 0b111_1111;
fn spoken_days(weekdays: i64) -> String {
if weekdays & EVERY_DAY == EVERY_DAY {
return "day".to_string();
}
WEEKDAY_NAMES
.iter()
.enumerate()
.filter(|(day, _)| weekdays & (1 << day) != 0)
.map(|(_, name)| *name)
.collect::<Vec<_>>()
.join(" and ")
}
fn clock_time(minute_of_day: i64) -> String {
format!("{}:{:02}", minute_of_day / 60, minute_of_day % 60)
}
fn add_timer(handler: String, repeat: Repeat, trigger: Trigger) {
let timer = Timer { handler, repeat, due: Some(repeat.first(Instant::now())), trigger };
TIMERS.with(|timers| timers.borrow_mut().push(timer));
}
thread_local! {
static TIMERS: RefCell<Vec<Timer>> = const { RefCell::new(Vec::new()) };
static RUNNING_HANDLERS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
static STAYING_ALLOWED: AtomicBool = AtomicBool::new(false);
thread_local! {
static EXIT_CODE: Cell<Option<i32>> = const { Cell::new(None) };
static FIRST_FRAME_ENDS_RUN: Cell<bool> = const { Cell::new(false) };
}
pub fn until_first_frame<T>(run: impl FnOnce() -> T) -> T {
FIRST_FRAME_ENDS_RUN.set(true);
let value = run();
FIRST_FRAME_ENDS_RUN.set(false);
value
}
pub fn end_at_first_frame() -> Result<()> {
match FIRST_FRAME_ENDS_RUN.get() {
true => Err(wasmtime::Error::new(ExitRequest(0))),
false => Ok(()),
}
}
#[derive(Debug)]
pub struct ExitRequest(pub i32);
impl std::fmt::Display for ExitRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "exit({})", self.0)
}
}
impl std::error::Error for ExitRequest {}
pub fn ended_by_exit<R>(outcome: &Result<R>) -> bool {
let code = outcome.as_ref().err().and_then(|error| error.downcast_ref::<ExitRequest>()).map(|request| request.0);
code.inspect(|code| EXIT_CODE.with(|kept| kept.set(Some(*code)))).is_some()
}
pub fn take_exit_code() -> Option<i32> {
EXIT_CODE.with(Cell::take)
}
pub fn forget_timers() {
TIMERS.with(|timers| timers.borrow_mut().clear());
}
pub fn listen(kind: i64, id: i64, argument: i64) {
let every = Repeat::Every(Duration::from_millis(argument.max(1) as u64));
let (repeat, trigger) = match kind {
LISTEN_FRAME => (every, Trigger::WindowShows { ran: false }),
LISTEN_CLOCK => {
let weekdays = argument / CLOCK_WEEKDAYS;
let clock = Repeat::Clock {
minute_of_day: argument % CLOCK_WEEKDAYS,
weekdays: if weekdays == 0 { EVERY_DAY } else { weekdays },
once: weekdays == 0,
};
(clock, Trigger::Always)
}
_ => (every, Trigger::Always),
};
add_timer(format!("{TIMER_HANDLER_PREFIX}{id}"), repeat, trigger);
}
pub fn await_ready(handler: String, awaited: String, ready: impl FnMut() -> bool + 'static) {
TIMERS.with(|timers| timers.borrow_mut().retain(|timer| timer.handler != handler));
add_timer(handler, Repeat::Every(AWAIT_CHECK_PERIOD), Trigger::Ready(awaited, Box::new(ready)));
}
const DAY: Duration = Duration::from_secs(24 * 3600);
const WEEK_DAYS: i64 = 7;
pub fn seconds_until(minute_of_day: i64, second_of_day: i64) -> i64 {
seconds_until_on(minute_of_day, EVERY_DAY, 0, second_of_day)
}
pub fn seconds_until_on(minute_of_day: i64, weekdays: i64, weekday: i64, second_of_day: i64) -> i64 {
let day = DAY.as_secs() as i64;
let weekdays = if weekdays & EVERY_DAY == 0 { EVERY_DAY } else { weekdays };
let today = minute_of_day * 60 - second_of_day;
(0..=WEEK_DAYS)
.map(|days_ahead| (days_ahead, today + days_ahead * day))
.find(|(days_ahead, seconds)| *seconds > 0 && weekdays & (1 << ((weekday + days_ahead) % WEEK_DAYS)) != 0)
.map_or(WEEK_DAYS * day, |(_, seconds)| seconds)
}
fn local_time() -> (i64, i64) {
local_time_at(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default())
}
pub fn millisecond_of_day() -> i64 {
let since = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
local_time_at(since).1 * 1000 + since.subsec_millis() as i64
}
fn local_time_at(since_epoch: Duration) -> (i64, i64) {
let now = since_epoch.as_secs() as i64;
let local = now + local_offset_at(now * 1000);
let day = DAY.as_secs() as i64;
const THURSDAY: i64 = 4; ((local.div_euclid(day) + THURSDAY) % WEEK_DAYS, local.rem_euclid(day))
}
pub fn local_offset_at(milliseconds: i64) -> i64 {
#[cfg(unix)]
{
let time = milliseconds.div_euclid(1000) as libc::time_t;
let mut local: libc::tm = unsafe { std::mem::zeroed() };
if !unsafe { libc::localtime_r(&time, &mut local) }.is_null() {
return local.tm_gmtoff as i64;
}
}
let _ = milliseconds;
0
}
pub fn watch_file(id: i64, path: String) {
let path = PathBuf::from(path);
let seen = stamp(&path);
add_timer(format!("{FILE_HANDLER_PREFIX}{id}"), Repeat::Every(FILE_CHECK_PERIOD), Trigger::FileChange(path, seen));
}
type FileStamp = (SystemTime, u64);
fn stamp(path: &PathBuf) -> Option<FileStamp> {
let metadata = std::fs::metadata(path).ok()?;
Some((metadata.modified().ok()?, metadata.len()))
}
fn next_due() -> Option<Instant> {
TIMERS.with(|timers| timers.borrow().iter().filter_map(|timer| timer.due).min())
}
fn due_handlers(handles_interrupt: bool) -> Vec<String> {
let mut due = vec![];
if handles_interrupt && interrupt::take() {
due.push(INTERRUPT_HANDLER.to_string());
}
let now = Instant::now();
TIMERS.with(|timers| {
let mut timers = timers.borrow_mut();
for timer in timers.iter_mut() {
let Some(when) = timer.due.filter(|when| *when <= now) else {
continue;
};
timer.due = timer.repeat.after(when, now);
if !timer.trigger.holds(&mut timer.due) {
continue;
}
due.push(timer.handler.clone());
}
timers.retain(|timer| timer.due.is_some());
});
due
}
pub fn run_due_handlers<S: AsContextMut>(
store: &mut S,
mut export: impl FnMut(&mut S, &str) -> Option<Func>,
) -> Result<()> {
let handles_interrupt = export(store, INTERRUPT_HANDLER).is_some();
let shared_checks = export(store, SHARED_HANDLER).map(|_| SHARED_HANDLER.to_string());
for name in due_handlers(handles_interrupt).into_iter().chain(shared_checks) {
let running = RUNNING_HANDLERS.with(|running| running.borrow().contains(&name));
let Some(handler) = export(store, &name).filter(|_| !running) else { continue };
RUNNING_HANDLERS.with(|running| running.borrow_mut().push(name.clone()));
let outcome = call_handler(store, handler, None, &mut export);
RUNNING_HANDLERS.with(|running| running.borrow_mut().retain(|other| *other != name));
outcome?;
}
Ok(())
}
fn call_handler<S: AsContextMut>(
store: &mut S,
handler: Func,
event: Option<i64>,
export: &mut impl FnMut(&mut S, &str) -> Option<Func>,
) -> Result<()> {
let mut arguments = vec![];
if let Some(parameter) = handler.ty(&*store).params().next() {
arguments.push(match (parameter, event) {
(wasmtime::ValType::I64, event) => Val::I64(event.unwrap_or(0)),
(_, Some(event)) => node_of(store, export, NEW_INT, &[Val::I64(event)])?,
(_, None) => node_of(store, export, NEW_EMPTY, &[])?,
});
}
let mut results = vec![Val::I64(0); handler.ty(&*store).results().len()];
handler.call(&mut *store, &arguments, &mut results)
}
fn node_of<S: AsContextMut>(
store: &mut S,
export: &mut impl FnMut(&mut S, &str) -> Option<Func>,
constructor: &str,
arguments: &[Val],
) -> Result<Val> {
let function = export(store, constructor).ok_or_else(|| wasmtime::Error::msg(format!("no {constructor}")))?;
let mut built = [Val::AnyRef(None)];
function.call(&mut *store, arguments, &mut built)?;
Ok(built[0])
}
pub fn run_exit_handler<S: AsContextMut>(
store: &mut S,
mut export: impl FnMut(&mut S, &str) -> Option<Func>,
) -> Result<()> {
let Some(handler) = export(store, EXIT_HANDLER) else {
return Ok(());
};
let code = EXIT_CODE.with(Cell::get).unwrap_or(0);
call_handler(store, handler, Some(code.into()), &mut export)
}
pub fn with_exit_handler<S: AsContextMut, R>(
outcome: Result<R>,
store: &mut S,
export: impl FnMut(&mut S, &str) -> Option<Func>,
) -> Result<Option<R>> {
let exited = ended_by_exit(&outcome);
let value = match outcome {
Ok(value) => Some(value),
Err(_) if exited => None,
Err(error) => return Err(error),
};
let handled = run_exit_handler(store, export);
if ended_by_exit(&handled) {
return Ok(None);
}
handled?;
Ok(value)
}
pub fn sleep_with_handlers<S: AsContextMut>(
store: &mut S,
duration: Duration,
mut export: impl FnMut(&mut S, &str) -> Option<Func>,
) -> Result<()> {
let end = Instant::now() + duration;
let checks_shared = export(store, SHARED_HANDLER).is_some();
loop {
let wake = next_due().map_or(end, |due| due.min(end));
let wake = if checks_shared { wake.min(Instant::now() + SHARED_CHECK_PERIOD) } else { wake };
let interrupted = interrupt::wait_until(wake);
run_due_handlers(store, &mut export)?;
if interrupted || Instant::now() >= end {
return Ok(());
}
}
}
pub fn allow_staying() {
STAYING_ALLOWED.store(true, Ordering::SeqCst);
}
fn spoken(period: Duration) -> String {
match (period.as_millis(), period.as_millis() % 1000) {
(1000, _) => "1 second".to_string(),
(milliseconds, 0) => format!("{} seconds", milliseconds / 1000),
(milliseconds, _) => format!("{milliseconds} ms"),
}
}
pub fn stay_while_listening<T>(store: &mut Store<T>, instance: &Instance) -> Result<()> {
if !STAYING_ALLOWED.load(Ordering::SeqCst) || next_due().is_none() {
return Ok(());
}
let listening: Vec<String> = TIMERS.with(|timers| {
timers
.borrow()
.iter()
.filter_map(|timer| match &timer.trigger {
Trigger::FileChange(path, _) => Some(format!("file {} change", path.display())),
Trigger::Ready(awaited, _) => Some(awaited.clone()),
Trigger::WindowShows { .. } => None,
Trigger::Always => Some(timer.repeat.spoken()),
})
.collect()
});
if !listening.is_empty() {
eprintln!("listening: {} (ctrl-c to stop)", listening.join(", "));
}
while let Some(due) = next_due() {
interrupt::wait_until(due);
run_due_handlers(store, |store, name| instance.get_func(&mut *store, name))?;
}
Ok(())
}
pub fn ctrl_c_ends_run(pending: bool, last: i64, now: i64) -> bool {
const HARD_EXIT_WITHIN_MILLISECONDS: i64 = 1000;
pending || (last != 0 && now - last < HARD_EXIT_WITHIN_MILLISECONDS)
}
mod interrupt {
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::Once;
use std::time::{Duration, Instant};
const EXIT_INTERRUPTED: i32 = 130; const SLEEP_SLICE: Duration = Duration::from_millis(10);
static INTERRUPTED: AtomicBool = AtomicBool::new(false);
static LAST_SIGNAL: AtomicI64 = AtomicI64::new(0);
static WATCHING: Once = Once::new();
pub fn wait_until(wake: Instant) -> bool {
if !WATCHING.is_completed() {
std::thread::sleep(wake.saturating_duration_since(Instant::now()));
return false;
}
while !INTERRUPTED.load(Ordering::SeqCst) {
let left = wake.saturating_duration_since(Instant::now());
if left.is_zero() {
return false;
}
std::thread::sleep(left.min(SLEEP_SLICE));
}
true
}
pub fn take() -> bool {
WATCHING.call_once(platform::install);
INTERRUPTED.swap(false, Ordering::SeqCst)
}
fn interrupted_at(now: i64) -> bool {
let last = LAST_SIGNAL.swap(now, Ordering::SeqCst);
super::ctrl_c_ends_run(INTERRUPTED.swap(true, Ordering::SeqCst), last, now)
}
#[cfg(unix)]
mod platform {
pub fn install() {
unsafe { libc::signal(libc::SIGINT, on_signal as extern "C" fn(libc::c_int) as libc::sighandler_t) };
}
extern "C" fn on_signal(_: libc::c_int) {
if super::interrupted_at(monotonic_milliseconds()) {
unsafe { libc::_exit(super::EXIT_INTERRUPTED) }
}
}
#[allow(clippy::unnecessary_cast)]
fn monotonic_milliseconds() -> i64 {
let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) };
now.tv_sec as i64 * 1000 + now.tv_nsec as i64 / 1_000_000
}
}
#[cfg(windows)]
mod platform {
use std::sync::OnceLock;
use std::time::Instant;
const CTRL_C_EVENT: u32 = 0;
const CTRL_BREAK_EVENT: u32 = 1;
const HANDLED: i32 = 1;
const NOT_HANDLED: i32 = 0;
static STARTED: OnceLock<Instant> = OnceLock::new();
#[link(name = "kernel32")]
extern "system" {
fn SetConsoleCtrlHandler(handler: Option<unsafe extern "system" fn(u32) -> i32>, add: i32) -> i32;
}
pub fn install() {
STARTED.get_or_init(Instant::now);
unsafe { SetConsoleCtrlHandler(Some(on_control), 1) };
}
unsafe extern "system" fn on_control(event: u32) -> i32 {
if event != CTRL_C_EVENT && event != CTRL_BREAK_EVENT {
return NOT_HANDLED;
}
let now = STARTED.get_or_init(Instant::now).elapsed().as_millis() as i64 + 1;
if super::interrupted_at(now) {
std::process::exit(super::EXIT_INTERRUPTED);
}
HANDLED
}
}
#[cfg(not(any(unix, windows)))]
mod platform {
pub fn install() {}
}
}