Sign in

    moon-lander-hosting

    Host lifecycle for Moon-Lander applications: services, stop requests and exit codes

    Download zip
    Author
    Version
    0.6.0
    License
    Apache-2.0
    Last updated
    13 hours ago
    Downloads
    188

    #moonrockz/moon-lander-hosting

    The host of a Moon-Lander application. A host builder holds the application name, the arguments, the hosted services and a services factory. build gives a Host, and run starts the services, runs the command and stops the services in a known order, with a clear exit code. On wasm and wasm-gc, run_embedded does the same without an event loop. Each run has a lifetime for events, stop requests and cancellation checks. The run resolves its starting settings, then parses arguments, loads configuration, builds services and runs hooks. The console driver in moonrockz/moon-lander-hosting/console changes console signals into a graceful stop and ends the process with the exit code.

    Import the root package with an explicit alias, because the module name has - in it. The application name comes from moonrockz/moon-lander-core:

    ///|
    import {
    "moonrockz/moon-lander-core" @lander_core,
    "moonrockz/moon-lander-hosting" @hosting,
    }

    The module moonrockz/moon-lander gives the batteries-included entry @lander.cli. This module gives the bare entry @hosting.builder:

    ///|
    test "build a host with one service" {
    let log = []
    let host : @hosting.Host[Unit] = "acme"
    |> @lander_core.AppName::parse
    |> @hosting.builder(["greet"])
    |> @hosting.service(
    "greeter",
    start=_ => log.push("greeter started"),
    stop=_ => log.push("greeter stopped"),
    )
    |> @hosting.build(_ => ())
    ignore(host)
    // The build runs no hook.
    assert_eq(log, [])
    }

    PackageTargetsContent
    moonrockz/moon-lander-hosting (@hosting)all; run on native and js; run_embedded on wasm and wasm-gcBuilder, command, hosted services, Host[S], run, SignalProbe, run_embedded, Lifetime, StopToken, StopReason, SignalKind, StopRequested, ServiceName, ExitCode and HostError
    moonrockz/moon-lander-hosting/argparse (@hosting_argparse)allCommand factory, global standard discovery flags, and the argparse types for the command (Command, OptionArg, FlagArg and others)
    moonrockz/moon-lander-hosting/console (@console)native, jsThe console driver run: signals, the second signal and the process exit. A C signal stub on native, Node.js listeners on js
    moonrockz/moon-lander-hosting/vendor/argparseallA copy of moonbitlang/core/argparse with Command::try_parse, until a pinned MoonBit release has it. Do not import it; use @hosting_argparse

    The moonrockz/moon-lander guide shows the full pipeline, the name and exit-code rules, and run.

    #Lifetime and stop requests

    Every hook and command gets the same Context[S]. Its lifetime raises started before the command, stopping before the stop hooks, and stopped after them. Handlers are synchronous and cannot raise. Each event occurs at most once in a run. A handler registered after its event runs at once, one time. Each new run has its own lifetime, even when it uses the same host.

    ///|
    test "register lifetime handlers from a hook" {
    let host : @hosting.Host[Unit] = "acme"
    |> @lander_core.AppName::parse
    |> @hosting.builder([])
    |> @hosting.service("events", start=ctx => {
    ctx.lifetime.on_started(() => ())
    ctx.lifetime.on_stopping(() => {
    // Completed, Requested, Signal, Embedder, StartFailed or Failed.
    ignore(ctx.lifetime.stop_reason())
    })
    ctx.lifetime.on_stopped(() => ())
    })
    |> @hosting.build(_ => ())
    ignore(host)
    }

    stop_application(exit_code?) asks the host to stop. During start, the current hook finishes; later start hooks and the command are skipped. Only services that started get stop hooks. During the command, a request cancels its child task. The command ends at its next suspension point and its defer blocks run before the stop phase. Shutdown runs inside @async.protect_from_cancel. A command can use the same function to protect a critical part of its work.

    The first event that ends the run sets stop_reason(). Later requests keep that reason and the first request's optional code, so all stop hooks read one reason. set_exit_code(code) sets an application result without asking to stop. The last call to set_exit_code or the first request's explicit code wins. A signal or failure takes precedence over the application code. The driver returns RunOutcome without ending the process.

    #Cancellation checks

    Use ctx.cancelled() to read the stop state, or ctx.check_cancelled() to raise StopRequested. ctx.lifetime.stop_token() gives the same checks to code that does not receive the context. These checks read stop requests, registered host probes, and, on native and js, the current async task's cancellation state. On native and js, read these checks inside an async task of the run. A probe stop reaches a suspended command only through the driver that owns the probe. A command or hook that raises StopRequested ends as a stop and adds no failure. A service whose starting or start hook raises it does not count as started.

    ///|
    test "a command requests stop and polls its token" {
    let command : @hosting.Hook[Unit] = ctx => {
    let token = ctx.lifetime.stop_token()
    ctx.lifetime.stop_application(exit_code=@hosting.ExitCode::success())
    token.check_cancelled()
    }
    ignore(command)
    }

    On js, Node.js cannot deliver a signal listener while a synchronous loop keeps running. Long synchronous loops must call @async.pause() at intervals to give the event loop time to run the listener. A cancellation check alone does not yield. On native, the console driver gives a signal probe that each check reads at once, also in a loop that never yields. A task cancellation takes effect at a suspension point.

    A console driver gives run a SignalProbe. The run registers it before the first hook, reads it in each cancellation check and every 50 ms, and changes the first signal into a stop request with the reason Signal(kind):

    ///|
    test "a console driver gives the run a signal probe" {
    let signal : Ref[@hosting.SignalKind?] = Ref(None)
    // On native and js: @hosting.run(host, command, signals=probe)
    let probe = @hosting.SignalProbe::new(() => signal.val)
    ignore(probe)
    }

    ///|
    test "a long command yields between batches" {
    let command : @hosting.Hook[Unit] = ctx => {
    for _ in 0..<100 {
    ctx.check_cancelled()
    // Process one bounded batch here.
    @async.pause()
    }
    }
    ignore(command)
    }

    #Shutdown timeout

    shutdown_timeout(limit) sets one budget for the full stop phase. The budget starts when the command task has ended. The default is 30 s, and a later step replaces an earlier value. Only native and js apply the limit. The wasm and wasm-gc drivers have no timer.

    shutdown_timeout_key(key) opts in to an application-named @config.KeyPath. The host reads it once after loading and before the factory, through FromConfig for TimeLimit. A value such as "100ms" overrides the builder limit. An absent key keeps the builder limit, or the default 30 s. Invalid text or a number gives a configuration error with code 2 before the factory or any hook. The framework reserves no timeout key.

    At the limit, the host cancels the current hook, skips the remaining stop hooks, raises the stopped event and reports ShutdownTimedOut. A hook that protects itself from cancellation, or does not yield, runs until it returns, and run waits for it.

    ///|
    test "set the shutdown timeout" {
    let host : @hosting.Host[Unit] = "acme"
    |> @lander_core.AppName::parse
    |> @hosting.builder([])
    |> @hosting.shutdown_timeout(@lander_core.TimeLimit::seconds(5))
    |> @hosting.build(_ => ())
    ignore(host)
    }

    #Exit codes and reports

    CodeCause
    0Clean run
    1Unhandled factory, hook or command failure; stop failure or timeout
    2Configuration or usage error in any phase
    130Interrupt signal stop
    143Terminate signal stop
    131Quit signal stop
    nApplication code from set_exit_code or stop_application(exit_code=...)

    Precedence is signal, configuration or usage error, unhandled failure, stop failure or timeout, application code, then 0. A later signal still sets the code and keeps the first stop reason. Later stop requests keep the first request's optional code. The last application code set by set_exit_code or the first request wins. ExitCode::from_int accepts 0 to 255.

    ///|
    test "application and signal codes" {
    let command : @hosting.Hook[Unit] = ctx => {
    ctx.lifetime.set_exit_code(@hosting.ExitCode::from_int(3))
    ctx.lifetime.stop_application(exit_code=@hosting.ExitCode::from_int(4))
    }
    assert_eq(@hosting.SignalKind::Interrupt.exit_code().to_int(), 130)
    ignore(command)
    }

    run reports each ShutdownTimedOut failure for every exit code, including signal codes 130, 143 and 131. It reports other failures only for code 1 or 2. Reports follow failure order. The default report writes one line to standard error: <application name>: <message>. A run with no failures writes nothing, including an application code of 1 or 2. HostError, WorkingDirectoryError, UsageError, FileTreeError, SuspendError, ShutdownTimedOut and ConfigError use their message() text; other errors use their Show text.

    error_report replaces the default report. Its async callback gets each reported failure in occurrence order with the final ExitCode, under the same rules. It can write a custom format such as JSON. Each call to the builder step replaces the earlier report.

    ///|
    test "replace the failure report" {
    let reports : Array[(String, @hosting.ExitCode)] = []
    let host : @hosting.Host[Unit] = "acme"
    |> @lander_core.AppName::parse
    |> @hosting.builder([])
    |> @hosting.error_report((error, code) => {
    let message = match error {
    @hosting.HostError::_ as e => e.message()
    @hosting.UsageError::_ as e => e.message()
    @hosting.ShutdownTimedOut::_ as e => e.message()
    _ => error.to_string()
    }
    reports.push((message, code))
    })
    |> @hosting.build(_ => ())
    let command : @hosting.Hook[Unit] = _ => raise @hosting.UsageError("expected a command")
    ignore((host, command))
    }

    #Starting settings and variables

    Each run uses one variable set and one absolute working directory. The builder accepts env_vars, working_dir and environment as starting settings. Omitted variables and directory come from the process on native and js, or the empty set and / on wasm and wasm-gc. Explicit values cause no process read. environment is a parsed @conventions.EnvironmentName default and enables the environments part of the host-owned conventions set, as does default_environment(name). Add app_dirs() or conventions(f) to supply that set. Without it, the host gives a note; conventions added inside configure keep their own environment settings. A bare builder with no environment default adds no discovery reports and gives no discovery behavior to --config or --project. Parse the name at the caller with @conventions.EnvironmentName::parse. The host takes the name from --environment, then <PREFIX>_ENVIRONMENT in the run snapshot, then the default. An invalid name ends the run with code 2 before the factory or any hook.

    The factory, hooks and command read environment.application_name, environment.environment_name and environment.content_root on their contexts. The is_environment(name), is_development(), is_staging() and is_production() checks ignore case. With no name, each check gives false. content_root(path) sets an absolute content root. Without it, the content root is the working directory of the run. Windows paths use / separators.

    A later env_vars or working_dir step replaces that setting. The factory, hooks and command read the final values through env_vars and working_dir on their contexts. The variable snapshot is an immutable @immut/sorted_map.SortedMap[String, String] in deterministic key order. Each builder step snapshots its input, so later changes to a caller map or JSON object do not change the host. A missing process working directory ends the run with WorkingDirectoryError and code 1 before any file read or hook.

    ///|
    test "set the variable prefix and starting settings" {
    let host : @hosting.Host[Unit] = @lander_core.AppName::parse("my-tool")
    |> @hosting.builder([], env_vars={ "MT_LOG__FORMAT": "json" },
    working_dir=@lander_core.AbsolutePath::parse("/work"))
    |> @hosting.env_prefix(@lander_core.EnvPrefix::parse("MT"))
    |> @hosting.reserve_env(["MT_LOG_DIR"])
    |> @hosting.app_dirs()
    |> @hosting.build(_ => ())
    ignore(host)
    }

    The prefix defaults to the application name in upper case, with - changed to _. An explicit prefix accepts only upper-case ASCII letters, digits and _, starting with an upper-case letter or _. The host adds a variable source at priority 600 and reserves <PREFIX>_ENVIRONMENT, <PREFIX>_HOME and the names from reserve_env. app_dirs() gives the same name and final prefix to conventions. skip_env_source() reports the source as skipped and keeps the variable set for argparse and all other readers.

    env_vars accepts a string map or a JSON object through IntoEnvVars. JSON strings stay as written; other values become JSON text, except null, which omits the variable. Nested objects stay whole and keys keep their spelling. A non-object root ends the run with UsageError and code 2 before any file read or hook.

    reserve_env takes full variable names. The source compares names after it converts each name to upper case. A later env_prefix step keeps these names as given; it does not join them to the new prefix. For example, reserve MT_TOKEN when the final prefix is MT:

    ///|
    test "reserve a full name before changing the prefix" {
    let host : @hosting.Host[Unit] = @lander_core.AppName::parse("my-tool")
    |> @hosting.builder([], env_vars={ "MT_TOKEN": "secret" })
    |> @hosting.reserve_env(["MT_TOKEN"])
    |> @hosting.env_prefix(@lander_core.EnvPrefix::parse("MT"))
    |> @hosting.build(_ => ())
    ignore(host)
    }

    #Configuration and services

    configure(fn(sources) { ... }) adds formats and application sources through @config.ConfigBuilder. Callbacks run in registration order after starting settings, argument parsing and environment resolution. Sources merge in this order: configure-callback sources in order, the host-owned conventions set (System 100 through UserOverride 400), the <PREFIX>_* source at 600, then the argparse overrides at 700 (group 14). Core has no builder trait, so use configure for core steps.

    Core load requires priorities that do not go down. With app_dirs() or conventions(f), a configure source must have priority 100 or less, such as defaults or an in-memory value at 0. Put a file at a higher priority in the conventions set, or leave out the host-owned set. A later change may order sources by priority.

    The run's variable set and working directory replace values that a callback sets; use env_vars or working_dir on the host builder.

    ///|
    test "configure application sources and conventions" {
    let host : @hosting.Host[Unit] = "acme"
    |> @lander_core.AppName::parse
    |> @hosting.builder([])
    |> @hosting.configure(fn(sources) raise {
    sources
    |> @config.defaults({ "log": { "format": "text" } })
    |> @conventions.add(@conventions.app_dirs("acme"))
    })
    |> @hosting.build(_ => ())
    ignore(host)
    }

    A conventions set added inside configure keeps its own options. It does not receive the standard host flags --environment, --config, --isolated and --project, or the profile selectors. Use app_dirs() for the host-owned standard set and conventions(fn(c) { ... }) to change it. These steps compose in pipeline order. A later app_dirs() replaces earlier convention parts. Without it, conventions starts from an empty base set with the final host name and prefix. The host-owned set receives the standard flags. For example, conventions(c => c |> @conventions.workspace()) after app_dirs() enables workspace member selection with --project. With neither step, a bare host adds no discovery reports.

    Native and js call @host.load to answer file requests from disk, using the run's variable snapshot and working directory. They ignore files(tree). On wasm and wasm-gc, files(tree) supplies the immutable snapshot to the pure @config.load. A complete tree gives the configuration. An incomplete tree raises FileTreeError::IncompleteFileTree with code 2 and preserves each requested path and request kind. Its report names every path. run_embedded ends with this code before the factory or any hook.

    #Configuration profiles

    Enable profiles with profiles(policy) after app_dirs(). Register profiles_option() and no_profiles_flag() on the root argparse command. Repeated --profiles postgres,metrics options concatenate in argument order. --no-profiles conflicts with those options and disables includes, defaults, groups and profile files. A bare builder adds no file discovery parts.

    The host reserves the exact <PREFIX>_PROFILES selector from ordinary variable mapping when its conventions enable profiles. A present empty or ASCII-whitespace selector disables profiles; an absent selector does not. env_prefix sets this prefix too. A custom profile provider inside configure keeps its own discovery options; explicitly reserve its selector with reserve_env.

    The factory, hooks and command read one recorded config.profiles() value. Use a parsed ProfileName to query membership and choose services in the factory:

    ///|
    test "choose a database service from recorded profile membership" {
    let postgres = @config.ProfileName::parse("postgres")
    let host : @hosting.Host[String] = @hosting.builder(
    @lander_core.AppName::parse("acme"), ["--profiles", "postgres"],
    env_vars=({} : Map[String, String]), working_dir=@lander_core.AbsolutePath::root())
    |> @hosting_argparse.command(name => @hosting_argparse.Command(name,
    options=[@hosting_argparse.profiles_option()],
    flags=[@hosting_argparse.no_profiles_flag()]))
    |> @hosting.configure(s => s |> @config.format(@toml.format()))
    |> @hosting.app_dirs()
    |> @hosting.profiles(@profiles.Policy::new())
    |> @hosting.service("database", start=ctx => {
    assert_eq(ctx.services == "postgres", ctx.config.profiles().contains(postgres))
    })
    |> @hosting.build(ctx =>
    if ctx.config.profiles().contains(postgres) { "postgres" } else { "memory" })
    ignore(host)
    }

    Profiles remain independent of HostEnvironment. A Production environment does not activate production. Profile file layers have priority 380, after environment files and before local and user overrides. Profile failures stop before the factory with configuration exit code 2. Invalid CLI names and lists give usage code 2 even when profiles are off. Help and version return before configuration callbacks or profile discovery.

    Custom source providers now return Expansion::new(sources, warnings=notes) instead of a public struct literal. They can supply immutable resolution metadata with its optional profiles argument. Providers that omit it keep state Off. Exhaustive matches on FileRole must handle ProfileFile(ProfileName).

    After loading, the factory runs once. Its BuildContext and every hook's and command's Context[S] share the final configuration, variable snapshot and working directory. Each hook, lifetime handler and command uses the same services record. Loading and timeout-decoding failures occur before the factory or any hook. The public argparse overrides layer at priority 700 arrives in task group 14.

    #Embedded driver on wasm and wasm-gc

    run_embedded(host, command, stop_probe?) is a synchronous driver for wasm and wasm-gc. It needs no event loop, so any WebAssembly runtime can run the host. It loads configuration from the files(tree) snapshot. With no env_vars or working_dir, the variable set is empty and the working directory is /. Hooks and the command run in the same order, with the same failure, stop-reason and exit-code rules as run. The function returns a RunOutcome and does not end the process.

    Each hook and the command run until they return. They must not wait: a hook or command that suspends fails with SuspendError and code 1, and the normal failure and stop rules apply. The driver has no signals and no shutdown timeout. The run stops through stop_application or through stop_probe. The host reads the probe in each cancellation check and after each hook. When the probe gives true, the run stops with the reason Embedder, and the stop phase runs.

    The default report writes <application name>: <message> lines with eprintln. On wasm, that is WASI standard error. On wasm-gc, core has no standard-error channel, so the line goes to standard output; use error_report to send it to a different place. A replacement report must not suspend. The driver ignores a report that raises or suspends.

    ///|
    test "prepare a host and a polling command for the embedded driver" {
    let stop = Ref(false)
    let host : @hosting.Host[Unit] = "acme"
    |> @lander_core.AppName::parse
    |> @hosting.builder([])
    |> @hosting.files(@config.FileTree::new(complete=true))
    |> @hosting.build(_ => ())
    let command : @hosting.Hook[Unit] = ctx => {
    while !ctx.cancelled() {
    // Process one bounded batch here, without a wait.
    stop.val = true
    }
    }
    // On wasm and wasm-gc:
    // let outcome = @hosting.run_embedded(host, command, stop_probe=() => stop.val)
    ignore((host, command))
    }

    ErrorReport

    type ErrorReport = async (Error, ExitCode) -> Unit

    A replacement failure report. It may write asynchronously, and receives the final exit code with each failure, in occurrence order. Each shutdown timeout is reported for every exit code. Other failures are reported only for code 1 or 2. If a report raises, the driver ignores its error and reports the next failure. A report error cannot change the outcome or make run raise.

    Hook

    type Hook[S] = async (Context[S]) -> Unit

    A hook of a hosted service, or the command of a run. It can wait, and it can raise an error.

    ReportWriter

    type ReportWriter = async (String) -> Unit

    A writer of the lines of the default failure report.

    IntoEnvVars

    pub trait IntoEnvVars {
    fn into_env_vars(Self) -> Map[String, String] raise UsageError
    }

    A variable set supplied as a string map or a JSON object. Conversion runs at the builder step and gives an owned snapshot. The run raises any stored conversion error before argument parsing or configuration I/O.
    impl IntoEnvVars for Json

    IntoServiceName

    pub trait IntoServiceName {
    fn into_service_name(Self) -> ServiceName raise HostError
    }

    A value that gives a service name: a ServiceName, or a String that ServiceName::parse reads. So service takes either, and parses text at the boundary. After that step, the host holds only the ServiceName.

    FileTreeError

    pub suberror FileTreeError {
    IncompleteFileTree(requests~ : Array[
    FileRequest
    ])
    }

    The injected configuration tree does not answer these file requests. Only the host makes this error; callers can inspect each request and path.

    FileTreeError::message

    fn FileTreeError::message(self : FileTreeError) -> String

    Text for people that names every requested path.

    HostError

    pub(all) suberror HostError {
    InvalidServiceName(text~ : String, reason~ : String)
    DuplicateService(name~ : ServiceName, existing~ : ServiceName)
    InvalidExitCode(code~ : Int)
    } derive(
    Debug
    )

    A fault in the program that sets up a host: a service name, a code or a service set that breaks its rules. An invalid application name is an @lander_core.AppNameError.

    HostError::message

    fn HostError::message(self : HostError) -> String

    Text for people that describes the fault.

    MappingError

    pub suberror MappingError {
    InvalidOverrideOption(input~ : String, reason~ : String)
    } derive(
    Debug
    )

    A mapping id is empty or is reserved for host discovery.

    ShutdownTimedOut

    pub suberror ShutdownTimedOut {
    ShutdownTimedOut(
    TimeLimit
    )
    }

    The stop phase used its full time budget. Only the host makes this error.

    ShutdownTimedOut::message

    fn ShutdownTimedOut::message(self : ShutdownTimedOut) -> String

    Text for people that names the shutdown time limit.

    StopRequested

    pub(all) suberror StopRequested

    A command or hook can raise this error to stop without a failure.

    SuspendError

    pub suberror SuspendError {
    HookSuspended(service~ : ServiceName, phase~ : HookPhase)
    CommandSuspended
    }

    A hook or the command suspended in run_embedded on wasm or wasm-gc. That driver has no event loop, so it cannot resume suspended work. Only the host makes this error.

    SuspendError::message

    fn SuspendError::message(self : SuspendError) -> String

    Text for people that names the step and tells how to correct it.

    UsageError

    pub(all) suberror UsageError {
    UsageError(String)
    }

    An application command can raise this error for invalid usage.

    UsageError::message

    fn UsageError::message(self : UsageError) -> String

    The usage error text for people or a custom report.

    WorkingDirectoryError

    pub suberror WorkingDirectoryError {
    MissingWorkingDirectory
    InvalidWorkingDirectory(
    AbsolutePathError
    )
    } derive(
    Debug
    )

    A process working directory that the host cannot read as an absolute path.

    WorkingDirectoryError::message

    fn WorkingDirectoryError::message(self : WorkingDirectoryError) -> String

    Describe the process directory failure for people.

    BuildContext

    The values that the services factory gets. The services record does not exist yet, so the factory gets no Context.

    Context

    The values that each hook and the command get. The host makes one context for each run, so each hook and the command see the same services record.

    Context::cancelled

    fn[S] Context::cancelled(self : Context[S]) -> Bool

    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.

    Context::check_cancelled

    fn[S] Context::check_cancelled(self : Context[S]) -> Unit raise StopRequested

    Raise StopRequested after a stop request or task cancellation.

    ExitCode

    pub struct ExitCode {
    // private fields
    } derive(Eq,
    Debug
    )

    The exit code of a run, in the range 0 to 255 that every platform keeps.

    ExitCode::equal

    fn ExitCode::equal(ExitCode, ExitCode) -> Bool

    ExitCode::failure

    fn ExitCode::failure() -> ExitCode

    The exit code 1: an unhandled failure.

    ExitCode::from_int

    fn ExitCode::from_int(code : Int) -> ExitCode raise HostError

    Make an exit code from a number. It raises InvalidExitCode when code is not in the range 0 to 255.

    ExitCode::not_equal

    fn ExitCode::not_equal(x : ExitCode, y : ExitCode) -> Bool

    ExitCode::success

    fn ExitCode::success() -> ExitCode

    The exit code 0: a clean run.

    ExitCode::to_int

    fn ExitCode::to_int(self : ExitCode) -> Int

    The exit code as a number for the process.

    ExitCode::to_repr

    HookPhase

    pub(all) enum HookPhase {
    Starting
    Start
    Started
    Stopping
    Stop
    Stopped
    } derive(Eq,
    Debug
    )

    The six hooks of a hosted service.
    impl Show for HookPhase

    HookPhase::equal

    fn HookPhase::equal(HookPhase, HookPhase) -> Bool

    HookPhase::not_equal

    fn HookPhase::not_equal(x : HookPhase, y : HookPhase) -> Bool

    HookPhase::output

    fn HookPhase::output(self : HookPhase, logger : &Logger) -> Unit

    HookPhase::to_string

    fn HookPhase::to_string(self : HookPhase) -> String

    Host

    pub struct Host[S] {
    // private fields
    }

    A host that is ready to run. build does no I/O, so the same host can run more than one time.

    HostBuilder

    pub struct HostBuilder[S] {
    // private fields
    }

    The settings of a host before the services factory exists. Each step gives a new builder, so a builder value does not change.

    HostEnvironment

    The immutable application name, optional environment name and content root of a run. Only the host makes this value, from parsed starting settings.

    HostEnvironment::is_development

    fn HostEnvironment::is_development(self : HostEnvironment) -> Bool

    Whether the environment is Development, without regard to letter case.

    HostEnvironment::is_environment

    Compare the environment name without regard to letter case. With no name, the result is false.

    HostEnvironment::is_production

    fn HostEnvironment::is_production(self : HostEnvironment) -> Bool

    Whether the environment is Production, without regard to letter case.

    HostEnvironment::is_staging

    fn HostEnvironment::is_staging(self : HostEnvironment) -> Bool

    Whether the environment is Staging, without regard to letter case.

    Lifetime

    pub struct Lifetime {
    // private fields
    }

    A handle to the events and stop state of one run. Only the host creates it.

    Lifetime::on_started

    fn Lifetime::on_started(self : Lifetime, handler : () -> Unit) -> Unit

    Register a synchronous handler. After started, it runs at once, one time.

    Lifetime::on_stopped

    fn Lifetime::on_stopped(self : Lifetime, handler : () -> Unit) -> Unit

    Register a synchronous handler. After stopped, it runs at once, one time.

    Lifetime::on_stopping

    fn Lifetime::on_stopping(self : Lifetime, handler : () -> Unit) -> Unit

    Register a synchronous handler. After stopping, it runs at once, one time.

    Lifetime::set_exit_code

    fn Lifetime::set_exit_code(self : Lifetime, code : ExitCode) -> Unit

    Set the application exit code. Signals and failures take precedence. The last set code or first request's explicit code wins.

    Lifetime::stop_application

    fn Lifetime::stop_application(self : Lifetime, exit_code? : ExitCode) -> Unit

    Ask the host to stop. The first request keeps its optional exit code.

    Lifetime::stop_reason

    fn Lifetime::stop_reason(self : Lifetime) -> StopReason?

    The first event that ended the run, or None until an event ends it.

    Lifetime::stop_token

    fn Lifetime::stop_token(self : Lifetime) -> StopToken

    Give a token for code that does not receive a Context.

    Mapping

    pub struct Mapping {
    // private fields
    }

    One explicit argument-to-key mapping with its declared value shape.

    MappingAction

    pub(all) enum MappingAction {
    Flag
    Count
    Set
    Append
    }

    The value shape of a mapped argument. Match the argparse declaration. Matches omits option actions and count entries whose value is zero.

    OverrideOption

    pub struct OverrideOption {
    // private fields
    } derive(Eq,
    Debug
    )

    An argument id that can supply a configuration override. The standard discovery ids cannot be mapped.

    OverrideOption::equal

    OverrideOption::not_equal

    fn OverrideOption::not_equal(x : OverrideOption, y : OverrideOption) -> Bool

    OverrideOption::parse

    fn OverrideOption::parse(text : String) -> OverrideOption raise MappingError

    Parse a non-empty argparse id. Reject environment, config, isolated project, profiles and no-profiles, which supply host discovery inputs rather than config values.

    OverrideOption::to_string

    fn OverrideOption::to_string(self : OverrideOption) -> String

    The argparse id of the mapping.

    RunOutcome

    pub struct RunOutcome {
    // private fields
    }

    The result of a run. The run does not end the process. Only the run makes an outcome, so the exit code always agrees with the failures.

    RunOutcome::exit_code

    fn RunOutcome::exit_code(self : RunOutcome) -> ExitCode

    The code selected by signal, failure and application-code precedence.

    RunOutcome::failures

    fn RunOutcome::failures(self : RunOutcome) -> Array[Error]

    Each failure of the factory, a hook or the command, and a shutdown timeout, in the order that it occurred. The array is a copy.

    ServiceName

    pub struct ServiceName {
    // private fields
    } derive(Eq, Hash,
    Debug
    )

    The name of a hosted service. It is safe as one component of a file path on Linux, macOS and Windows:

    • It starts with a character that Unicode allows at the start of an identifier (XID_Start: letters and letter numbers).
    • The other characters are identifier characters (XID_Continue: letters, combining marks, digits, _ and a few connector characters such as ·), - or .. So it has no path separator, no reserved punctuation, no whitespace and no control or direction character.
    • It has no invisible identifier character, such as a zero-width joiner, a variation selector or a Hangul filler. A name that looks blank or looks like another name cannot occur, and macOS HFS+ cannot ignore part of it.
    • It does not end with ., which Windows removes.
    • It is not a Windows device name such as CON or com1, also not before an extension, as in nul.log.
    • It is at most 128 bytes in UTF-8, so a file name with a prefix or an extension stays under the 255-byte limit.

    The parse does not normalize the text. Two names that differ only in their Unicode normalization form are different names.
    impl Show for ServiceName

    ServiceName::equal

    fn ServiceName::equal(ServiceName, ServiceName) -> Bool

    ServiceName::hash

    fn ServiceName::hash(self : ServiceName) -> Int

    ServiceName::hash_combine

    fn ServiceName::hash_combine(ServiceName, Hasher) -> Unit

    ServiceName::into_service_name

    fn ServiceName::into_service_name(self : ServiceName) -> ServiceName raise HostError

    ServiceName::not_equal

    fn ServiceName::not_equal(x : ServiceName, y : ServiceName) -> Bool

    ServiceName::output

    fn ServiceName::output(self : ServiceName, logger : &Logger) -> Unit

    ServiceName::parse

    fn ServiceName::parse(text : String) -> ServiceName raise HostError

    Parse a service name, for example db, db.primary or données.

    ServiceName::to_string

    fn ServiceName::to_string(self : ServiceName) -> String

    SignalKind

    pub(all) enum SignalKind {
    Interrupt
    Terminate
    Quit
    } derive(Eq,
    Debug
    )

    A signal whose identity the host knows.

    SignalKind::equal

    fn SignalKind::equal(SignalKind, SignalKind) -> Bool

    SignalKind::exit_code

    fn SignalKind::exit_code(self : SignalKind) -> ExitCode

    The portable process exit code for this signal.

    SignalKind::not_equal

    fn SignalKind::not_equal(x : SignalKind, y : SignalKind) -> Bool

    SignalProbe

    pub struct SignalProbe {
    // private fields
    }

    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.

    SignalProbe::new

    fn SignalProbe::new(read : () -> SignalKind?) -> SignalProbe

    Make a probe from a read that gives the signal that asked the process to stop, or None before a signal.

    StopReason

    pub(all) enum StopReason {
    Completed
    Requested
    Signal(SignalKind)
    Embedder
    StartFailed(Error)
    Failed(Error)
    }

    The first event that ended the run. Later requests keep this reason.

    StopToken

    pub struct StopToken {
    // private fields
    }

    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.

    StopToken::cancelled

    fn StopToken::cancelled(self : StopToken) -> Bool

    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.

    StopToken::check_cancelled

    fn StopToken::check_cancelled(self : StopToken) -> Unit raise StopRequested

    Raise StopRequested when this token reports cancellation.

    app_dirs

    fn[S] app_dirs(builder : HostBuilder[S]) -> HostBuilder[S]

    Enable application directory discovery with the final name and prefix.

    build

    fn[S] build(builder : HostBuilder[S], factory : (BuildContext) -> S raise) -> Host[S]

    Give the host with the services factory. The run calls the factory one time, after the configuration load and before the first hook.

    builder

    Start a bare host builder with the application name and the command-line arguments. The builder adds the prefixed variable source, but no services. Omitted variables and directory come from the process on native and js, and from the empty set and / on wasm and wasm-gc, once per run. environment sets a default name and enables the environments part. Environment files need a primary part such as app_dirs(). Parse the name with @conventions.EnvironmentName::parse at the caller boundary.

    command

    fn[S] command(builder : HostBuilder[S], make : (String) ->
    Command
    , mappings? : Array[Mapping]) -> HostBuilder[S]

    Set the command factory. The run gives it the application name and parses its command once, before configuration and hooks.

    configure

    Add application sources and formats to the core configuration pipeline. Callbacks run in registration order, after arguments and environment resolution. Sources merge in this order: configure callbacks, the host-owned conventions set (System 100 through UserOverride 400), the _* source at 600, then argparse overrides at 700. Core load requires priorities that do not go down. With app_dirs() or conventions(f), configure sources must have priority 100 or less, such as defaults or an in-memory value at 0. Put a higher-priority file in the conventions set, or leave out the host-owned set. Conventions added here keep their own discovery options and do not see standard host flags. Use app_dirs() and conventions() for host-owned flag-aware discovery. The run's variable set and working directory replace values that a callback sets; use env_vars or working_dir on the host builder. A callback error ends the run before the factory or any hook.

    content_root

    Set the content root to an absolute path. A later step replaces the value. Without this step, the host uses the working directory of the run.

    conventions

    Transform the host-owned convention set, starting from the empty base set unless app_dirs() has supplied its standard parts. Steps compose in order; a later app_dirs() replaces earlier parts. The final set receives the run's standard flags and resolved environment name.

    default_environment

    Set the default environment and enable the environments part. Environment files need a primary part such as app_dirs(). The flag and the run's prefixed variable still take precedence. A later step replaces the default. Parse the name with @conventions.EnvironmentName::parse.

    env_prefix

    fn[S] env_prefix(builder : HostBuilder[S], prefix :
    EnvPrefix
    ) -> HostBuilder[S]

    Replace the prefix for both the variable source and application directories.

    env_vars

    fn[S] env_vars(builder : HostBuilder[S], vars : &IntoEnvVars) -> HostBuilder[S]

    Replace the variable set for every reader. Conversion errors end the run with a usage error, before configuration I/O or hooks.

    error_report

    fn[S] error_report(builder : HostBuilder[S], report : async (Error, ExitCode) -> Unit) -> HostBuilder[S]

    Replace the default standard-error report. At the end of a run, the driver calls this for each shutdown timeout for every exit code, and for each other failure only for code 1 or 2. Reports follow failure order. If a report raises, the driver ignores its error and reports the next failure. A report error cannot change the outcome or make run raise.

    files

    Supply the immutable file snapshot for wasm and wasm-gc. An incomplete snapshot ends configuration loading with usage code 2 and the requested paths in its error message. Native and js ignore this snapshot and read disk through the config host adapter.

    map

    Map a parsed argument id to a parsed configuration key. action must match the argument declaration: Flag, Count, Set or Append. The default is Set. Append keeps a list with one value; Count keeps an integer even at zero. With parse, convert each text value, or the boolean/count text for a flag.

    profiles

    Enable configuration profiles on the host-owned conventions set. This step adds no discovery parts; use app_dirs to discover primary files. A later app_dirs replaces earlier convention steps, including this policy.

    report_writer

    fn[S] report_writer(host : Host[S], writer : async (String) -> Unit) -> Host[S]

    Send each line of the default failure report, <name>: <message>, to writer instead of standard error. The run reports the same failures in the same order. An application report from error_report keeps its behavior and the writer gets no line. If the writer raises, the driver ignores its error and reports the next failure. The host logging bridge uses this step to write the default report through its logger.

    reserve_env

    fn[S] reserve_env(builder : HostBuilder[S], names : Array[String]) -> HostBuilder[S]

    Add full variable names that the source must not turn into keys. Names are compared after conversion to upper case, as in the variable source. A later env_prefix step does not change these names.

    run

    async fn[S] run(host : Host[S], command : async (Context[S]) -> Unit, signals? : SignalProbe) -> RunOutcome

    Run the host without ending the process. Hooks share one context and services record. Start hooks run in registration order, stop hooks in reverse order. Stop requests cancel the command task and its defers finish before shutdown. Shutdown runs protected from outside cancellation, within one time budget. It reports each shutdown timeout for every exit code, and other failures only for code 1 or 2, in failure order, through error_report or as <name>: <message> on standard error. Report errors do not change the outcome.

    A help or version request of the arguments writes the argparse text one time with println and returns code 0 with the reason Requested, before the configuration load, the factory and the hooks. The console driver then ends the process with that code.

    A console driver gives signals. The run registers the probe before the first hook, so each cancellation check reads it, and a watcher task reads it every 50 ms until the run ends. The first signal is a stop request with the reason Signal(kind), and it cancels the command task.

    service

    fn[S] service(builder : HostBuilder[S], name : &IntoServiceName, starting? : async (Context[S]) -> Unit, start? : async (Context[S]) -> Unit, started? : async (Context[S]) -> Unit, stopping? : async (Context[S]) -> Unit, stop? : async (Context[S]) -> Unit, stopped? : async (Context[S]) -> Unit) -> HostBuilder[S] raise HostError

    Add a hosted service with six optional hooks. The host runs all starting, then all start, then all started hooks in registration order. After the command it runs all stopping, then all stop, then all stopped hooks in reverse order, for started services only. A service counts as started when its start hook returns without an error, or when the service has no start hook, the start phase reached its starting step, and its starting hook succeeded or is absent. A service that the start phase did not reach is not started. A start failure skips the remaining start hooks and the command. A stop failure does not skip later hooks.

    Each service has its own name, without regard to letter case. A duplicate raises DuplicateService. name is a ServiceName, or a String that the step parses; a bad name raises InvalidServiceName.

    shutdown_timeout

    fn[S] shutdown_timeout(builder : HostBuilder[S], limit :
    TimeLimit
    ) -> HostBuilder[S]

    Set one time budget for the full stop phase, after the command task ends. The default is 30 seconds. A later step replaces an earlier value. Only the native and js drivers apply the limit. The wasm and wasm-gc drivers have no timers. The host cancels the current hook at the limit. A hook that protects itself from cancellation, or does not yield, runs until it returns, and run waits for it.

    shutdown_timeout_key

    fn[S] shutdown_timeout_key(builder : HostBuilder[S], key :
    KeyPath
    ) -> HostBuilder[S]

    Read this application-named key once after loading, before the factory. Its TimeLimit value overrides shutdown_timeout; an absent value keeps the builder limit, which defaults to 30 seconds. A bad value is a configuration error and no factory or hook runs.

    skip_env_source

    fn[S] skip_env_source(builder : HostBuilder[S]) -> HostBuilder[S]

    Skip only the prefixed configuration source. Other readers keep the set.

    to_overrides

    Convert only explicit Argv and Env values into the priority-700 source. Search the selected subcommand path from the root; a deeper explicit value for the same id wins. A Default value never hides an explicit ancestor. The application still dispatches subcommands. Conversion failures give UsageError before the host loads configuration or creates services.

    working_dir

    Replace the absolute working directory. The last step wins.