Skip to main content

warp_runtime/
system_signals.rs

1//! System signals (notes/system_signals.md): handlers of events from outside the program, exported functions the
2//! runtime calls at check points: `signal_poll()` (at the start and end of main and at each loop start of a program
3//! with such handlers) and `sleep`, which wakes for them. `on interrupt {…}` is `on·interrupt`, run after a ctrl-c:
4//! the first poll installs the ctrl-c handler (Unix SIGINT, Windows SetConsoleCtrlHandler), which only sets a flag; a
5//! second ctrl-c within a second, or before the handler ran, ends the run at once (exit code 130), so a handler that
6//! ignores ctrl-c never makes a program unstoppable. `on every 5 seconds {…}` is `on·every·0`, started by `signal_listen(0, 0, 5000)` where the handler is
7//! declared. `on file "notes.txt" change {…}` is `on·file·0`, started by `signal_watch(0, "notes.txt")`: a timer of
8//! FILE_CHECK_PERIOD that fires only when the file's modification time or size changed (it appeared, was written, or went).
9//! `users := fetch url` runs on·fetch·0 once the reply arrived (await_ready, src/web/fetches.rs).
10//! A run that allows it (`warp run`, built executables) stays after main while a timer or watch lives.
11use 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
22/// The constructors a handler's `event` is built with: exports the stub calls besides the handlers
23pub const NEW_EMPTY: &str = "new_empty";
24pub const NEW_INT: &str = "new_int";
25/// How often a watched file's modification time is read: polling, no file-system events yet
26const FILE_CHECK_PERIOD: Duration = Duration::from_millis(100);
27/// How often an awaited thing (await_ready) is looked at
28const AWAIT_CHECK_PERIOD: Duration = Duration::from_millis(10);
29/// How often a sleeping program with listeners on shared values (on·shared) looks at them
30const SHARED_CHECK_PERIOD: Duration = Duration::from_millis(10);
31
32struct Timer {
33	handler: String,
34	repeat: Repeat,
35	/// None once a one-shot timer fired
36	due: Option<Instant>,
37	trigger: Trigger,
38}
39
40/// What must hold, besides the time, for a due timer to run its handler
41enum Trigger {
42	Always,
43	/// a watched file and its stamp when last read: the handler runs only when that changed
44	FileChange(PathBuf, Option<FileStamp>),
45	/// something awaited (a fetch's reply), described for "listening: …": the handler runs once, when `ready()`
46	Ready(String, Box<dyn FnMut() -> bool>),
47	/// the paint window shows (frames): the handler runs while it does and the timer ends as it closes; without a
48	/// window it runs once (`ran`), as a single paint would, so a headless run ends
49	WindowShows {
50		ran: bool,
51	},
52}
53
54impl Trigger {
55	/// Whether the handler runs now: a changed file, an awaited thing ready (which ends the timer)
56	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	/// a local time of day on the weekdays of the mask (bit 0 Sunday), every week or only `once`
88	Clock {
89		minute_of_day: i64,
90		weekdays: i64,
91		once: bool,
92	},
93}
94
95impl Repeat {
96	/// The first time from `now`: a clock is read anew each time, so a change of the local time (summer time) counts
97	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	/// The time after the one `due` fired at `now` (one call however late); None after a one-shot
108	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	/// `every 5 seconds`, `every monday at 9:00`, `at 9:00`
117	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	/// the timers of the run on this thread
155	static TIMERS: RefCell<Vec<Timer>> = const { RefCell::new(Vec::new()) };
156	/// the handlers running now on this thread: a due one still running skips its turn, else its loop's signal poll
157	/// would call it again inside itself, without end (card timer-handler)
158	static RUNNING_HANDLERS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
159}
160static STAYING_ALLOWED: AtomicBool = AtomicBool::new(false);
161
162thread_local! {
163	/// the code of the `exit` that ended the last run on this thread
164	static EXIT_CODE: Cell<Option<i32>> = const { Cell::new(None) };
165	/// a prerender's run (until_first_frame): its first frame or sleep ends it
166	static FIRST_FRAME_ENDS_RUN: Cell<bool> = const { Cell::new(false) };
167}
168
169/// The run of `warp build --site`'s prerender (src/web/site.rs, card site-build): an animation's first frame or sleep ends
170/// main as `exit` does, so the page is read as it stands then, and an endless loop builds too
171pub 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
178/// Before a frame (paint) or a sleep: the end of a prerender's run (until_first_frame)
179pub 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/// `exit(code)` (P121): the error that unwinds the run; the runner ends the run with ø and keeps the code
187#[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
198/// Whether the run ended by `exit`: the code is kept for take_exit_code
199pub 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
204/// The code of the `exit` that ended the last run on this thread (once)
205pub fn take_exit_code() -> Option<i32> {
206	EXIT_CODE.with(Cell::take)
207}
208
209/// A new run on this thread starts without timers
210pub fn forget_timers() {
211	TIMERS.with(|timers| timers.borrow_mut().clear());
212}
213
214/// `signal_listen(kind, id, argument)`: the handler on·every·id runs every `argument` milliseconds (LISTEN_EVERY), each
215/// frame of that many milliseconds while the paint window shows, once without one (LISTEN_FRAME), or at a local time
216/// of day (LISTEN_CLOCK: weekdays · CLOCK_WEEKDAYS + minute of day, weekdays a mask with bit 0 Sunday, 0 once)
217pub 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
235/// The handler `handler` runs once, at the first check point after `ready()` holds (a fetch's reply arrived); it
236/// replaces a timer of that handler still waiting (a fetch started anew)
237pub 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
245/// Seconds from `second_of_day` to the next `minute_of_day` (a whole day when it is now)
246pub 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
250/// Seconds from `second_of_day` of `weekday` (0 Sunday) to the next `minute_of_day` on a weekday of the mask (a whole
251/// week when that is now and the mask has only this day); an empty mask is every day
252pub 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
262/// The local weekday (0 Sunday) and seconds since local midnight (UTC where the platform gives no time zone)
263fn local_time() -> (i64, i64) {
264	local_time_at(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default())
265}
266
267/// The local time of day in milliseconds since midnight (system value "time of day", the word `time`)
268pub 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; // 1970-01-01
278	((local.div_euclid(day) + THURSDAY) % WEEK_DAYS, local.rem_euclid(day))
279}
280
281/// Seconds east of UTC in the environment's time zone (TZ, else the system's) at the instant `milliseconds` after
282/// 1970: daylight saving included. UTC where the platform gives no time zone
283pub 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
296/// `signal_watch(id, path)`: the handler on·file·id runs when the file changes from now on
297pub 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
303/// Modification time and size: a rewrite within one tick of a coarse clock still changes the size, mostly
304type 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
315/// The handlers to run now: on·interrupt after a ctrl-c (watched only by a program with that handler: any other
316/// ends at a ctrl-c as usual), then each due timer's (rescheduled; one call however late)
317fn 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
340/// Run the due handlers, then the checks of the listeners on shared values (each check point looks at them);
341/// `export` finds a function of the instance by name
342pub 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	// a handler that reads `event` gets it (the exit code of `on exit`, P134), as a number or a Node; else ø
367	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
378/// The Node a constructor export of the program builds (`new_empty()`, `new_int(n)`)
379fn 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
391/// `on exit {…}`: the run ends (main returned, the timers stopped, or `exit(code)`), so on·exit runs once; an `exit`
392/// inside it ends it with that code (ended_by_exit keeps it)
393pub 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
404/// The outcome of a run and its exit handler: the handler runs after a normal end and after `exit(code)` (whose code
405/// stays, unless the handler exits with its own), never after a failure
406pub 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
425/// Sleep for `duration`, running the handlers that come due on the way; a ctrl-c ends the sleep early
426pub 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
444/// `warp run` and built executables: a program may stay after main while it listens
445pub fn allow_staying() {
446	STAYING_ALLOWED.store(true, Ordering::SeqCst);
447}
448
449/// `5 seconds`, `1 second`, `200 ms`
450fn 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
458/// After main: while a timer lives (and the run allows it), wait for the next due handler and run it
459pub 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	// a window's frames say nothing: the window shows that the program goes on, and closing it ends the run
464	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
486/// A ctrl-c ends the run at once, instead of reaching `on interrupt`, when the one before is still `pending` (the
487/// program did not take it yet) or came `last` (monotonic milliseconds, 0: none yet) less than a second before `now`
488pub 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
493/// `on interrupt`: the first poll installs the platform's ctrl-c handler (Unix SIGINT, Windows SetConsoleCtrlHandler),
494/// which only sets a flag or ends the run (ctrl_c_ends_run)
495mod 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; // 128 + SIGINT, what a shell reports for a ctrl-c
501	/// How often a sleep of a watching program looks for a ctrl-c
502	const SLEEP_SLICE: Duration = Duration::from_millis(10);
503	static INTERRUPTED: AtomicBool = AtomicBool::new(false);
504	/// when the last ctrl-c came, monotonic milliseconds (0: none yet)
505	static LAST_SIGNAL: AtomicI64 = AtomicI64::new(0);
506	static WATCHING: Once = Once::new();
507
508	/// Sleep until `wake`; a watching program wakes early at a ctrl-c: whether one came
509	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	/// Whether a ctrl-c came since the last call; the first call starts watching
525	pub fn take() -> bool {
526		WATCHING.call_once(platform::install);
527		INTERRUPTED.swap(false, Ordering::SeqCst)
528	}
529
530	/// A ctrl-c at `now`: the flag set, or whether it ends the run (atomics only: async-signal-safe)
531	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		/// async-signal-safe (atomics, clock_gettime, _exit)
543		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		// time_t and c_long are i64 on 64-bit Linux, i32 on 32-bit targets: the casts are needed there
550		#[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	/// Windows calls the handler on a thread of its own, so it may use std
559	#[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		/// ctrl-c and ctrl-break set the flag; closing the console and the others keep Windows' default
581		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			// +1: 0 means no ctrl-c yet
586			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	/// no ctrl-c here (wasm): ctrl-c ends the run as before
595	#[cfg(not(any(unix, windows)))]
596	mod platform {
597		pub fn install() {}
598	}
599}