///|
// Only the wasm and wasm-gc driver makes Embedder, so native and js never
// construct it (6). Remove when the build system can limit a constructor to targets.
#warnings("-6")
priv enum StopRequest {
Requested
Signal(SignalKind)
Embedder
}
///|
priv enum Phase {
Starting
Start
Started
Stopping
Stop
Stopped
} derive(Eq, Debug)
///|
priv enum Event {
Started
Stopping
Stopped
} derive(Eq, Debug)
///|
/// Only Engine::start makes a reference, from a service's registration index.
priv struct ServiceRef {
index : Int
} derive(Eq, Debug)
///|
// Debug supports white-box tests only, and only `run` (native, js) compares
// actions with Eq, so wasm and wasm-gc do not use it. Remove when the build
// system can limit derived methods to tests and targets.
#warnings("-1")
priv enum Action {
Hook(ServiceRef, Phase)
RunCommand
Raise(Event)
} derive(Eq, Debug)
///|
priv enum Step {
Run(Pending, Action)
Finish(RunOutcome)
}
///|
/// Marking a service is internal work, so absent hooks produce no driver step.
priv enum Instruction {
Emit(Action)
MarkStarted(ServiceRef)
}
///|
priv enum EnginePhase {
Starting
Command
Stopping
Finished(RunOutcome)
}
///|
priv struct Engine {
instructions : Array[Instruction]
stop_at : Int
stopped_at : Int
started : Array[Bool]
failures : Array[Error]
mut position : Int
mut phase : EnginePhase
mut reason : StopReason?
mut requested_code : ExitCode?
mut signal : SignalKind?
mut stop_requested : Bool
mut timed_out : Bool
}
///|
/// The cached reply belongs to this token, even after later tokens advance.
priv struct Pending {
engine : Engine
action : Action
mut reply : Step?
}
///|
fn[S] HostedService::hook(self : HostedService[S], phase : Phase) -> Hook[S]? {
match phase {
Starting => self.starting
Start => self.start
Started => self.started
Stopping => self.stopping
Stop => self.stop
Stopped => self.stopped
}
}
///|
fn[S] Engine::start(services : Array[HostedService[S]]) -> Step {
let instructions = []
for phase in [Phase::Starting, Start, Started] {
for index, service in services {
let reference = ServiceRef::{ index }
if service.hook(phase) is Some(_) {
instructions.push(Emit(Hook(reference, phase)))
}
if phase == Starting && service.start is None {
instructions.push(MarkStarted(reference))
}
if phase == Start && service.start is Some(_) {
instructions.push(MarkStarted(reference))
}
}
}
instructions.push(Emit(Raise(Started)))
instructions.push(Emit(RunCommand))
let stop_at = instructions.length()
instructions.push(Emit(Raise(Stopping)))
for phase in [Phase::Stopping, Stop, Stopped] {
for index = services.length() - 1; index >= 0; index = index - 1 {
if services[index].hook(phase) is Some(_) {
instructions.push(Emit(Hook(ServiceRef::{ index }, phase)))
}
}
}
let stopped_at = instructions.length()
instructions.push(Emit(Raise(Stopped)))
let engine : Engine = {
instructions, stop_at, stopped_at, started: Array::make(services.length(), false), failures: [], position: 0,
phase: Starting, reason: None, requested_code: None, signal: None, stop_requested: false, timed_out: false,
}
engine.next()
}
///|
fn Engine::next(self : Engine) -> Step {
if self.phase is Finished(outcome) {
return Finish(outcome)
}
while self.position < self.instructions.length() {
let instruction = self.instructions[self.position]
self.position += 1
match instruction {
MarkStarted(reference) => self.started[reference.index] = true
Emit(action) => {
match action {
Hook(reference, Stopping | Stop | Stopped) =>
if !self.started[reference.index] { continue }
Raise(Stopping) => {
self.phase = Stopping
if self.reason is None { self.reason = Some(Completed) }
}
RunCommand => self.phase = Command
_ => ()
}
return Run({ engine: self, action, reply: None }, action)
}
}
}
let outcome : RunOutcome = {
exit_code: select_exit_code(self.signal, self.failures, self.timed_out, self.requested_code),
timed_out: self.timed_out,
failures: self.failures.copy(),
reason: self.reason.unwrap_or(Completed),
requested_code: self.requested_code,
signal: self.signal,
}
self.phase = Finished(outcome)
Finish(outcome)
}
///|
priv enum ActionResult {
Returned
Interrupted
Failed(Error)
}
///|
priv enum Reply {
Done
Failed(Error)
StopRequested(StopRequest, ExitCode?, ActionResult, SignalKind?)
TimedOut(@lander_core.TimeLimit, SignalKind?, ActionResult)
}
///|
fn Pending::respond(self : Pending, reply : Reply) -> Step {
if self.reply is Some(step) { return step }
let engine = self.engine
match reply {
Done => ()
Failed(error) => {
if self.action is Raise(_) { abort("failed reply to a Raise step") }
engine.failures.push(error)
match engine.phase {
Starting => {
if engine.reason is None { engine.reason = Some(StartFailed(error)) }
engine.position = engine.stop_at
}
Command => {
if engine.reason is None { engine.reason = Some(Failed(error)) }
}
_ => ()
}
}
StopRequested(request, code, result, observed_signal) => {
if result is Failed(_) && self.action is Raise(_) { abort("failed reply to a Raise step") }
if request is Signal(signal) { engine.signal = Some(signal) }
if observed_signal is Some(signal) { engine.signal = Some(signal) }
if !engine.stop_requested {
engine.stop_requested = true
if engine.reason is None {
engine.reason = Some(match request { Requested => Requested; Signal(kind) => Signal(kind); Embedder => Embedder })
}
engine.requested_code = code
}
if result is Failed(error) { engine.failures.push(error) }
if engine.phase is Starting {
// The current hook succeeded. Consume its mark before skipping startup.
if result is Returned && engine.position < engine.instructions.length() &&
engine.instructions[engine.position] is MarkStarted(reference) &&
self.action is Hook(current, Starting | Start) && current == reference {
engine.started[reference.index] = true
}
engine.position = engine.stop_at
}
}
TimedOut(limit, observed_signal, result) => {
match engine.phase {
Starting => abort("timed_out outside the stop phase: Starting")
Command => abort("timed_out outside the stop phase: Command")
Finished(_) => abort("timed_out outside the stop phase: Finished")
Stopping => ()
}
if observed_signal is Some(signal) { engine.signal = Some(signal) }
if result is Failed(error) { engine.failures.push(error) }
engine.timed_out = true
engine.failures.push(ShutdownTimedOut(limit))
// A timeout while raising Stopped must not raise the event twice.
engine.position = if engine.position > engine.stopped_at { engine.position } else { engine.stopped_at }
}
}
let next = engine.next()
self.reply = Some(next)
next
}
///|
fn Pending::done(self : Pending) -> Step { self.respond(Done) }
///|
fn Pending::failed(self : Pending, error : Error) -> Step { self.respond(Failed(error)) }
///|
fn Pending::stop_requested(self : Pending, request : StopRequest, code : ExitCode?, result~ : ActionResult = Returned, observed_signal? : SignalKind) -> Step {
self.respond(StopRequested(request, code, result, observed_signal))
}
///|
// Only `run` (native, js) calls this. wasm and wasm-gc have no timer, so it is
// unused there. Remove when the build system can limit a function to targets.
#warnings("-1")
fn Pending::timed_out(self : Pending, limit~ : @lander_core.TimeLimit, observed_signal? : SignalKind, result~ : ActionResult = Interrupted) -> Step { self.respond(TimedOut(limit, observed_signal, result)) }