1use crate::host_words::{
12 CLOCK_WEEKDAYS, EXIT_HANDLER, FILE_HANDLER_PREFIX, INTERRUPT_HANDLER, LISTEN_CLOCK, LISTEN_FRAME, SHARED_HANDLER,
13 TIMER_HANDLER_PREFIX,
14};
15use std::cell::{Cell, RefCell};
16use std::path::PathBuf;
17use std::sync::atomic::{AtomicBool, Ordering};
18use std::time::SystemTime;
19use std::time::{Duration, Instant};
20use wasmtime::{AsContextMut, Func, Instance, Result, Store, Val};
21
22pub const NEW_EMPTY: &str = "new_empty";
24pub const NEW_INT: &str = "new_int";
25const FILE_CHECK_PERIOD: Duration = Duration::from_millis(100);
27const AWAIT_CHECK_PERIOD: Duration = Duration::from_millis(10);
29const SHARED_CHECK_PERIOD: Duration = Duration::from_millis(10);
31
32struct Timer {
33 handler: String,
34 repeat: Repeat,
35 due: Option<Instant>,
37 trigger: Trigger,
38}
39
40enum Trigger {
42 Always,
43 FileChange(PathBuf, Option<FileStamp>),
45 Ready(String, Box<dyn FnMut() -> bool>),
47 WindowShows {
50 ran: bool,
51 },
52}
53
54impl Trigger {
55 fn holds(&mut self, due: &mut Option<Instant>) -> bool {
57 match self {
58 Trigger::Always => true,
59 Trigger::FileChange(path, seen) => {
60 let current = stamp(path);
61 let changed = current != *seen;
62 *seen = current;
63 changed
64 }
65 Trigger::Ready(_, ready) => {
66 let holds = ready();
67 if holds {
68 *due = None;
69 }
70 holds
71 }
72 Trigger::WindowShows { ran } => {
73 let holds = crate::system_values::window_shows() || !*ran;
74 *ran = true;
75 if !holds {
76 *due = None;
77 }
78 holds
79 }
80 }
81 }
82}
83
84#[derive(Clone, Copy)]
85enum Repeat {
86 Every(Duration),
87 Clock {
89 minute_of_day: i64,
90 weekdays: i64,
91 once: bool,
92 },
93}
94
95impl Repeat {
96 fn first(self, now: Instant) -> Instant {
98 match self {
99 Repeat::Every(period) => now + period,
100 Repeat::Clock { minute_of_day, weekdays, .. } => {
101 let (weekday, second_of_day) = local_time();
102 now + Duration::from_secs(seconds_until_on(minute_of_day, weekdays, weekday, second_of_day) as u64)
103 }
104 }
105 }
106
107 fn after(self, due: Instant, now: Instant) -> Option<Instant> {
109 match self {
110 Repeat::Every(period) => Some((due + period).max(now)),
111 Repeat::Clock { once: true, .. } => None,
112 Repeat::Clock { .. } => Some(self.first(now)),
113 }
114 }
115
116 fn spoken(self) -> String {
118 match self {
119 Repeat::Every(period) => format!("every {}", spoken(period)),
120 Repeat::Clock { minute_of_day, once: true, .. } => format!("at {}", clock_time(minute_of_day)),
121 Repeat::Clock { minute_of_day, weekdays, once: false } => {
122 format!("every {} at {}", spoken_days(weekdays), clock_time(minute_of_day))
123 }
124 }
125 }
126}
127
128const WEEKDAY_NAMES: [&str; 7] = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
129pub const EVERY_DAY: i64 = 0b111_1111;
130
131fn spoken_days(weekdays: i64) -> String {
132 if weekdays & EVERY_DAY == EVERY_DAY {
133 return "day".to_string();
134 }
135 WEEKDAY_NAMES
136 .iter()
137 .enumerate()
138 .filter(|(day, _)| weekdays & (1 << day) != 0)
139 .map(|(_, name)| *name)
140 .collect::<Vec<_>>()
141 .join(" and ")
142}
143
144fn clock_time(minute_of_day: i64) -> String {
145 format!("{}:{:02}", minute_of_day / 60, minute_of_day % 60)
146}
147
148fn add_timer(handler: String, repeat: Repeat, trigger: Trigger) {
149 let timer = Timer { handler, repeat, due: Some(repeat.first(Instant::now())), trigger };
150 TIMERS.with(|timers| timers.borrow_mut().push(timer));
151}
152
153thread_local! {
154 static TIMERS: RefCell<Vec<Timer>> = const { RefCell::new(Vec::new()) };
156 static RUNNING_HANDLERS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
159}
160static STAYING_ALLOWED: AtomicBool = AtomicBool::new(false);
161
162thread_local! {
163 static EXIT_CODE: Cell<Option<i32>> = const { Cell::new(None) };
165 static FIRST_FRAME_ENDS_RUN: Cell<bool> = const { Cell::new(false) };
167}
168
169pub fn until_first_frame<T>(run: impl FnOnce() -> T) -> T {
172 FIRST_FRAME_ENDS_RUN.set(true);
173 let value = run();
174 FIRST_FRAME_ENDS_RUN.set(false);
175 value
176}
177
178pub fn end_at_first_frame() -> Result<()> {
180 match FIRST_FRAME_ENDS_RUN.get() {
181 true => Err(wasmtime::Error::new(ExitRequest(0))),
182 false => Ok(()),
183 }
184}
185
186#[derive(Debug)]
188pub struct ExitRequest(pub i32);
189
190impl std::fmt::Display for ExitRequest {
191 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 write!(formatter, "exit({})", self.0)
193 }
194}
195
196impl std::error::Error for ExitRequest {}
197
198pub fn ended_by_exit<R>(outcome: &Result<R>) -> bool {
200 let code = outcome.as_ref().err().and_then(|error| error.downcast_ref::<ExitRequest>()).map(|request| request.0);
201 code.inspect(|code| EXIT_CODE.with(|kept| kept.set(Some(*code)))).is_some()
202}
203
204pub fn take_exit_code() -> Option<i32> {
206 EXIT_CODE.with(Cell::take)
207}
208
209pub fn forget_timers() {
211 TIMERS.with(|timers| timers.borrow_mut().clear());
212}
213
214pub fn listen(kind: i64, id: i64, argument: i64) {
218 let every = Repeat::Every(Duration::from_millis(argument.max(1) as u64));
219 let (repeat, trigger) = match kind {
220 LISTEN_FRAME => (every, Trigger::WindowShows { ran: false }),
221 LISTEN_CLOCK => {
222 let weekdays = argument / CLOCK_WEEKDAYS;
223 let clock = Repeat::Clock {
224 minute_of_day: argument % CLOCK_WEEKDAYS,
225 weekdays: if weekdays == 0 { EVERY_DAY } else { weekdays },
226 once: weekdays == 0,
227 };
228 (clock, Trigger::Always)
229 }
230 _ => (every, Trigger::Always),
231 };
232 add_timer(format!("{TIMER_HANDLER_PREFIX}{id}"), repeat, trigger);
233}
234
235pub fn await_ready(handler: String, awaited: String, ready: impl FnMut() -> bool + 'static) {
238 TIMERS.with(|timers| timers.borrow_mut().retain(|timer| timer.handler != handler));
239 add_timer(handler, Repeat::Every(AWAIT_CHECK_PERIOD), Trigger::Ready(awaited, Box::new(ready)));
240}
241
242const DAY: Duration = Duration::from_secs(24 * 3600);
243const WEEK_DAYS: i64 = 7;
244
245pub fn seconds_until(minute_of_day: i64, second_of_day: i64) -> i64 {
247 seconds_until_on(minute_of_day, EVERY_DAY, 0, second_of_day)
248}
249
250pub fn seconds_until_on(minute_of_day: i64, weekdays: i64, weekday: i64, second_of_day: i64) -> i64 {
253 let day = DAY.as_secs() as i64;
254 let weekdays = if weekdays & EVERY_DAY == 0 { EVERY_DAY } else { weekdays };
255 let today = minute_of_day * 60 - second_of_day;
256 (0..=WEEK_DAYS)
257 .map(|days_ahead| (days_ahead, today + days_ahead * day))
258 .find(|(days_ahead, seconds)| *seconds > 0 && weekdays & (1 << ((weekday + days_ahead) % WEEK_DAYS)) != 0)
259 .map_or(WEEK_DAYS * day, |(_, seconds)| seconds)
260}
261
262fn local_time() -> (i64, i64) {
264 local_time_at(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default())
265}
266
267pub fn millisecond_of_day() -> i64 {
269 let since = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default();
270 local_time_at(since).1 * 1000 + since.subsec_millis() as i64
271}
272
273fn local_time_at(since_epoch: Duration) -> (i64, i64) {
274 let now = since_epoch.as_secs() as i64;
275 let local = now + local_offset_at(now * 1000);
276 let day = DAY.as_secs() as i64;
277 const THURSDAY: i64 = 4; ((local.div_euclid(day) + THURSDAY) % WEEK_DAYS, local.rem_euclid(day))
279}
280
281pub fn local_offset_at(milliseconds: i64) -> i64 {
284 #[cfg(unix)]
285 {
286 let time = milliseconds.div_euclid(1000) as libc::time_t;
287 let mut local: libc::tm = unsafe { std::mem::zeroed() };
288 if !unsafe { libc::localtime_r(&time, &mut local) }.is_null() {
289 return local.tm_gmtoff as i64;
290 }
291 }
292 let _ = milliseconds;
293 0
294}
295
296pub fn watch_file(id: i64, path: String) {
298 let path = PathBuf::from(path);
299 let seen = stamp(&path);
300 add_timer(format!("{FILE_HANDLER_PREFIX}{id}"), Repeat::Every(FILE_CHECK_PERIOD), Trigger::FileChange(path, seen));
301}
302
303type FileStamp = (SystemTime, u64);
305
306fn stamp(path: &PathBuf) -> Option<FileStamp> {
307 let metadata = std::fs::metadata(path).ok()?;
308 Some((metadata.modified().ok()?, metadata.len()))
309}
310
311fn next_due() -> Option<Instant> {
312 TIMERS.with(|timers| timers.borrow().iter().filter_map(|timer| timer.due).min())
313}
314
315fn due_handlers(handles_interrupt: bool) -> Vec<String> {
318 let mut due = vec![];
319 if handles_interrupt && interrupt::take() {
320 due.push(INTERRUPT_HANDLER.to_string());
321 }
322 let now = Instant::now();
323 TIMERS.with(|timers| {
324 let mut timers = timers.borrow_mut();
325 for timer in timers.iter_mut() {
326 let Some(when) = timer.due.filter(|when| *when <= now) else {
327 continue;
328 };
329 timer.due = timer.repeat.after(when, now);
330 if !timer.trigger.holds(&mut timer.due) {
331 continue;
332 }
333 due.push(timer.handler.clone());
334 }
335 timers.retain(|timer| timer.due.is_some());
336 });
337 due
338}
339
340pub fn run_due_handlers<S: AsContextMut>(
343 store: &mut S,
344 mut export: impl FnMut(&mut S, &str) -> Option<Func>,
345) -> Result<()> {
346 let handles_interrupt = export(store, INTERRUPT_HANDLER).is_some();
347 let shared_checks = export(store, SHARED_HANDLER).map(|_| SHARED_HANDLER.to_string());
348 for name in due_handlers(handles_interrupt).into_iter().chain(shared_checks) {
349 let running = RUNNING_HANDLERS.with(|running| running.borrow().contains(&name));
350 let Some(handler) = export(store, &name).filter(|_| !running) else { continue };
351 RUNNING_HANDLERS.with(|running| running.borrow_mut().push(name.clone()));
352 let outcome = call_handler(store, handler, None, &mut export);
353 RUNNING_HANDLERS.with(|running| running.borrow_mut().retain(|other| *other != name));
354 outcome?;
355 }
356 Ok(())
357}
358
359fn call_handler<S: AsContextMut>(
360 store: &mut S,
361 handler: Func,
362 event: Option<i64>,
363 export: &mut impl FnMut(&mut S, &str) -> Option<Func>,
364) -> Result<()> {
365 let mut arguments = vec![];
366 if let Some(parameter) = handler.ty(&*store).params().next() {
368 arguments.push(match (parameter, event) {
369 (wasmtime::ValType::I64, event) => Val::I64(event.unwrap_or(0)),
370 (_, Some(event)) => node_of(store, export, NEW_INT, &[Val::I64(event)])?,
371 (_, None) => node_of(store, export, NEW_EMPTY, &[])?,
372 });
373 }
374 let mut results = vec![Val::I64(0); handler.ty(&*store).results().len()];
375 handler.call(&mut *store, &arguments, &mut results)
376}
377
378fn node_of<S: AsContextMut>(
380 store: &mut S,
381 export: &mut impl FnMut(&mut S, &str) -> Option<Func>,
382 constructor: &str,
383 arguments: &[Val],
384) -> Result<Val> {
385 let function = export(store, constructor).ok_or_else(|| wasmtime::Error::msg(format!("no {constructor}")))?;
386 let mut built = [Val::AnyRef(None)];
387 function.call(&mut *store, arguments, &mut built)?;
388 Ok(built[0])
389}
390
391pub fn run_exit_handler<S: AsContextMut>(
394 store: &mut S,
395 mut export: impl FnMut(&mut S, &str) -> Option<Func>,
396) -> Result<()> {
397 let Some(handler) = export(store, EXIT_HANDLER) else {
398 return Ok(());
399 };
400 let code = EXIT_CODE.with(Cell::get).unwrap_or(0);
401 call_handler(store, handler, Some(code.into()), &mut export)
402}
403
404pub fn with_exit_handler<S: AsContextMut, R>(
407 outcome: Result<R>,
408 store: &mut S,
409 export: impl FnMut(&mut S, &str) -> Option<Func>,
410) -> Result<Option<R>> {
411 let exited = ended_by_exit(&outcome);
412 let value = match outcome {
413 Ok(value) => Some(value),
414 Err(_) if exited => None,
415 Err(error) => return Err(error),
416 };
417 let handled = run_exit_handler(store, export);
418 if ended_by_exit(&handled) {
419 return Ok(None);
420 }
421 handled?;
422 Ok(value)
423}
424
425pub fn sleep_with_handlers<S: AsContextMut>(
427 store: &mut S,
428 duration: Duration,
429 mut export: impl FnMut(&mut S, &str) -> Option<Func>,
430) -> Result<()> {
431 let end = Instant::now() + duration;
432 let checks_shared = export(store, SHARED_HANDLER).is_some();
433 loop {
434 let wake = next_due().map_or(end, |due| due.min(end));
435 let wake = if checks_shared { wake.min(Instant::now() + SHARED_CHECK_PERIOD) } else { wake };
436 let interrupted = interrupt::wait_until(wake);
437 run_due_handlers(store, &mut export)?;
438 if interrupted || Instant::now() >= end {
439 return Ok(());
440 }
441 }
442}
443
444pub fn allow_staying() {
446 STAYING_ALLOWED.store(true, Ordering::SeqCst);
447}
448
449fn spoken(period: Duration) -> String {
451 match (period.as_millis(), period.as_millis() % 1000) {
452 (1000, _) => "1 second".to_string(),
453 (milliseconds, 0) => format!("{} seconds", milliseconds / 1000),
454 (milliseconds, _) => format!("{milliseconds} ms"),
455 }
456}
457
458pub fn stay_while_listening<T>(store: &mut Store<T>, instance: &Instance) -> Result<()> {
460 if !STAYING_ALLOWED.load(Ordering::SeqCst) || next_due().is_none() {
461 return Ok(());
462 }
463 let listening: Vec<String> = TIMERS.with(|timers| {
465 timers
466 .borrow()
467 .iter()
468 .filter_map(|timer| match &timer.trigger {
469 Trigger::FileChange(path, _) => Some(format!("file {} change", path.display())),
470 Trigger::Ready(awaited, _) => Some(awaited.clone()),
471 Trigger::WindowShows { .. } => None,
472 Trigger::Always => Some(timer.repeat.spoken()),
473 })
474 .collect()
475 });
476 if !listening.is_empty() {
477 eprintln!("listening: {} (ctrl-c to stop)", listening.join(", "));
478 }
479 while let Some(due) = next_due() {
480 interrupt::wait_until(due);
481 run_due_handlers(store, |store, name| instance.get_func(&mut *store, name))?;
482 }
483 Ok(())
484}
485
486pub fn ctrl_c_ends_run(pending: bool, last: i64, now: i64) -> bool {
489 const HARD_EXIT_WITHIN_MILLISECONDS: i64 = 1000;
490 pending || (last != 0 && now - last < HARD_EXIT_WITHIN_MILLISECONDS)
491}
492
493mod interrupt {
496 use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
497 use std::sync::Once;
498 use std::time::{Duration, Instant};
499
500 const EXIT_INTERRUPTED: i32 = 130; const SLEEP_SLICE: Duration = Duration::from_millis(10);
503 static INTERRUPTED: AtomicBool = AtomicBool::new(false);
504 static LAST_SIGNAL: AtomicI64 = AtomicI64::new(0);
506 static WATCHING: Once = Once::new();
507
508 pub fn wait_until(wake: Instant) -> bool {
510 if !WATCHING.is_completed() {
511 std::thread::sleep(wake.saturating_duration_since(Instant::now()));
512 return false;
513 }
514 while !INTERRUPTED.load(Ordering::SeqCst) {
515 let left = wake.saturating_duration_since(Instant::now());
516 if left.is_zero() {
517 return false;
518 }
519 std::thread::sleep(left.min(SLEEP_SLICE));
520 }
521 true
522 }
523
524 pub fn take() -> bool {
526 WATCHING.call_once(platform::install);
527 INTERRUPTED.swap(false, Ordering::SeqCst)
528 }
529
530 fn interrupted_at(now: i64) -> bool {
532 let last = LAST_SIGNAL.swap(now, Ordering::SeqCst);
533 super::ctrl_c_ends_run(INTERRUPTED.swap(true, Ordering::SeqCst), last, now)
534 }
535
536 #[cfg(unix)]
537 mod platform {
538 pub fn install() {
539 unsafe { libc::signal(libc::SIGINT, on_signal as extern "C" fn(libc::c_int) as libc::sighandler_t) };
540 }
541
542 extern "C" fn on_signal(_: libc::c_int) {
544 if super::interrupted_at(monotonic_milliseconds()) {
545 unsafe { libc::_exit(super::EXIT_INTERRUPTED) }
546 }
547 }
548
549 #[allow(clippy::unnecessary_cast)]
551 fn monotonic_milliseconds() -> i64 {
552 let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
553 unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) };
554 now.tv_sec as i64 * 1000 + now.tv_nsec as i64 / 1_000_000
555 }
556 }
557
558 #[cfg(windows)]
560 mod platform {
561 use std::sync::OnceLock;
562 use std::time::Instant;
563
564 const CTRL_C_EVENT: u32 = 0;
565 const CTRL_BREAK_EVENT: u32 = 1;
566 const HANDLED: i32 = 1;
567 const NOT_HANDLED: i32 = 0;
568 static STARTED: OnceLock<Instant> = OnceLock::new();
569
570 #[link(name = "kernel32")]
571 extern "system" {
572 fn SetConsoleCtrlHandler(handler: Option<unsafe extern "system" fn(u32) -> i32>, add: i32) -> i32;
573 }
574
575 pub fn install() {
576 STARTED.get_or_init(Instant::now);
577 unsafe { SetConsoleCtrlHandler(Some(on_control), 1) };
578 }
579
580 unsafe extern "system" fn on_control(event: u32) -> i32 {
582 if event != CTRL_C_EVENT && event != CTRL_BREAK_EVENT {
583 return NOT_HANDLED;
584 }
585 let now = STARTED.get_or_init(Instant::now).elapsed().as_millis() as i64 + 1;
587 if super::interrupted_at(now) {
588 std::process::exit(super::EXIT_INTERRUPTED);
589 }
590 HANDLED
591 }
592 }
593
594 #[cfg(not(any(unix, windows)))]
596 mod platform {
597 pub fn install() {}
598 }
599}