///|
/// The first event that ended the run. Later requests keep this reason.
pub(all) enum StopReason {
Completed
Requested
Signal(SignalKind)
Embedder
StartFailed(Error)
Failed(Error)
}
///|
/// A signal whose identity the host knows.
pub(all) enum SignalKind { Interrupt; Terminate; Quit } derive(Eq, Debug)
///|
pub extend SignalKind with Eq::{equal, not_equal}
///|
pub extend SignalKind with @debug.Debug::{to_repr}
///|
/// The signal read of a console driver. `run` reads it in each cancellation
/// check and every 50 ms, and changes the first signal into a stop request
/// with the reason `Signal(kind)`. The read must be cheap and must not raise.
// Only `run` (native, js) reads the probe; wasm and wasm-gc have no signals (7).
// Remove when the build system can limit a field read to targets.
#warnings("-7")
pub struct SignalProbe {
priv request : () -> StopRequest?
}
///|
/// Make a probe from a read that gives the signal that asked the process to
/// stop, or `None` before a signal.
pub fn SignalProbe::new(read : () -> SignalKind?) -> SignalProbe {
{ request: () => read().map(kind => StopRequest::Signal(kind)) }
}
///|
/// A command or hook can raise this error to stop without a failure.
pub(all) suberror StopRequested
///|
priv struct EventHandlers {
mut raised : Bool
handlers : Array[() -> Unit]
}
///|
fn EventHandlers::new() -> EventHandlers { { raised: false, handlers: [] } }
///|
fn EventHandlers::register(self : EventHandlers, handler : () -> Unit) -> Unit {
if self.raised { handler() } else { self.handlers.push(handler) }
}
///|
fn EventHandlers::raise_event(self : EventHandlers) -> Unit {
if self.raised { return }
self.raised = true
let handlers = self.handlers.copy()
self.handlers.clear()
for handler in handlers { handler() }
}
///|
/// A handle to the events and stop state of one run. Only the host creates it.
pub struct Lifetime {
priv started : EventHandlers
priv stopping : EventHandlers
priv stopped : EventHandlers
priv mut reason : StopReason?
priv mut request : StopRequest?
priv mut signal : SignalKind?
priv mut requested_code : ExitCode?
priv mut exit_code : ExitCode?
priv probes : Array[() -> StopRequest?]
priv task_cancelled : Ref[() -> Bool]
priv cancel_command : Ref[(() -> Unit)?]
}
///|
fn Lifetime::new() -> Lifetime {
{ started: EventHandlers::new(), stopping: EventHandlers::new(), stopped: EventHandlers::new(),
reason: None, request: None, signal: None, requested_code: None, exit_code: None, probes: [], task_cancelled: Ref(() => false), cancel_command: Ref(None) }
}
///|
/// Register a synchronous handler. After started, it runs at once, one time.
pub fn Lifetime::on_started(self : Lifetime, handler : () -> Unit) -> Unit { self.started.register(handler) }
///|
/// Register a synchronous handler. After stopping, it runs at once, one time.
pub fn Lifetime::on_stopping(self : Lifetime, handler : () -> Unit) -> Unit { self.stopping.register(handler) }
///|
/// Register a synchronous handler. After stopped, it runs at once, one time.
pub fn Lifetime::on_stopped(self : Lifetime, handler : () -> Unit) -> Unit { self.stopped.register(handler) }
///|
fn Lifetime::raise_event(self : Lifetime, event : Event) -> Unit {
match event {
Started => self.started.raise_event()
Stopping => self.stopping.raise_event()
Stopped => self.stopped.raise_event()
}
}
///|
fn Lifetime::request_stop(self : Lifetime, request : StopRequest, code : ExitCode?) -> Unit {
if self.request is None {
self.request = Some(request)
self.requested_code = code
if code is Some(_) { self.exit_code = code }
if self.reason is None {
self.reason = Some(match request { Requested => Requested; Signal(kind) => Signal(kind); Embedder => Embedder })
}
}
if request is Signal(signal) { self.signal = Some(signal) }
if self.cancel_command.val is Some(cancel) { cancel() }
}
///|
/// Ask the host to stop. The first request keeps its optional exit code.
pub fn Lifetime::stop_application(self : Lifetime, exit_code? : ExitCode) -> Unit {
self.request_stop(Requested, exit_code)
}
///|
/// Set the application exit code. Signals and failures take precedence.
/// The last set code or first request's explicit code wins.
pub fn Lifetime::set_exit_code(self : Lifetime, code : ExitCode) -> Unit { self.exit_code = Some(code) }
///|
/// The first event that ended the run, or None until an event ends it.
pub fn Lifetime::stop_reason(self : Lifetime) -> StopReason? { self.reason }
///|
/// A handle with the same stop checks as its lifetime.
/// During a run on native and js, read its checks inside an async task of the run.
pub struct StopToken { priv lifetime : Lifetime }
///|
/// Give a token for code that does not receive a Context.
pub fn Lifetime::stop_token(self : Lifetime) -> StopToken { { lifetime: self } }
///|
fn Lifetime::register_stop_probe(self : Lifetime, probe : () -> StopRequest?) -> Unit {
self.probes.push(probe)
}
///|
fn Lifetime::probe_stop(self : Lifetime) -> Bool {
for probe in self.probes {
if probe() is Some(request) { self.request_stop(request, None) }
}
self.request is Some(_)
}
///|
/// Raise StopRequested when this token reports cancellation.
pub fn StopToken::check_cancelled(self : StopToken) -> Unit raise StopRequested {
if self.cancelled() { raise StopRequested }
}
///|
/// Read the lifetime's stop state and the current task's cancellation state.
/// On native and js, read this check inside an async task of the run.
pub fn[S] Context::cancelled(self : Context[S]) -> Bool { self.lifetime.cancelled() }
///|
/// Raise StopRequested after a stop request or task cancellation.
pub fn[S] Context::check_cancelled(self : Context[S]) -> Unit raise StopRequested {
if self.cancelled() { raise StopRequested }
}
///|
/// Read stop requests, registered probes and the current task's cancellation.
/// During a run on native and js, read this check inside an async task of the run.
/// Outside a running driver the token reads only its lifetime and probes.
pub fn StopToken::cancelled(self : StopToken) -> Bool {
self.lifetime.cancelled()
}
///|
fn Lifetime::cancelled(self : Lifetime) -> Bool {
self.probe_stop() || (self.task_cancelled.val)()
}
///|
/// The reply of a driver after one step: read the probes, then reply with the
/// first stop request, the failure or done. Both drivers use this rule.
fn Lifetime::reply(self : Lifetime, pending : Pending, result : ActionResult) -> Step {
ignore(self.probe_stop())
let next = if self.request is Some(request) {
pending.stop_requested(request, self.requested_code, result~, observed_signal?=self.signal)
} else if result is Failed(error) { pending.failed(error) }
else { pending.done() }
if pending.engine.reason is Some(reason) { self.reason = Some(reason) }
next
}
///|
/// Start the engine, after the probe read. A probe stop that arrived during
/// configuration loading or the services factory means that the start phase
/// reached no service: no service is started and no hook runs. The engine then
/// starts without services, so it marks none started, and the reply to its first
/// step goes to the stop events.
fn[S] Lifetime::start_engine(self : Lifetime, services : Array[HostedService[S]]) -> Step {
guard self.probe_stop() && self.request is Some(request) else { return Engine::start(services) }
let none : Array[HostedService[S]] = []
guard Engine::start(none) is Run(pending, _) else { abort("an engine without services raises Started first") }
pending.stop_requested(request, self.requested_code, result=Interrupted, observed_signal?=self.signal)
}