Run a check before every command¶
Pre-run hooks run after argument parsing succeeds and before the matched command
executes. The self-update policy check is one, which is how rtb-cli avoids
depending on rtb-update.
Register a hook¶
PreRunHook is a plain fn(App) -> PreRunFuture, so the hook has to box its own
future:
use linkme::distributed_slice;
use rtb_app::app::App;
use rtb_app::command::{PreRunFuture, PreRunHook, BUILTIN_PRERUN_HOOKS};
#[distributed_slice(BUILTIN_PRERUN_HOOKS)]
static CHECK_WORKSPACE: PreRunHook = |app: App| -> PreRunFuture {
Box::pin(async move {
if app.metadata.name.is_empty() {
return Err(miette::miette!("tool metadata has no name"));
}
Ok(())
})
};
The same linkme rules apply as for commands — see
Register a command for the
cannot find linkme in the crate root fix and the #[linkme(crate = …)] escape
hatch.
Decide whether the hook may abort the run¶
A hook returning Err stops the run before the command executes. That is the
whole decision:
- A hook enforcing something — a required credential, a workspace precondition —
should return
Errand stop the user doing damage. - A hook that is advisory — "there is a newer version available" — must swallow its
own errors and return
Ok. Otherwise a flaky network call blocks a user from running an unrelated command.
Box::pin(async move {
if let Err(e) = check_for_updates(&app).await {
tracing::debug!("update check failed, continuing: {e}");
}
Ok(()) // advisory: never blocks the command
})
Keep it fast¶
Every hook runs on every invocation, including --help. Anything doing network I/O
needs a short timeout and a cheap early exit — the update policy check throttles on
update_check_interval for exactly this reason.
Do not depend on ordering¶
Link-time registration order across crates is not deterministic. A hook that assumes another hook has already run will work in one build and not in the next, with no warning either way. If two pieces of work have to happen in order, put them in one hook.
Hooks are fn pointers, not closures, so a hook cannot capture anything. Everything
it needs comes off the App it is handed — which is a constraint worth knowing
before you design around captured state.