self-update-extras
Self-updating support for CLI binaries.
self-update-extras provides three small, composable wrappers around any type that
implements self_update's
ReleaseUpdate trait. Each wrapper is itself a ReleaseUpdate, so they layer
over a backend — or over each other — and can be used anywhere a
ReleaseUpdate is expected.
throttle::Update— limits how often update checks run, recording the time of the last check in a throttle file in the system temp directory.restart::Update— re-executes the process with the freshly installed binary after a successful update, using a guard environment variable to prevent restart loops.silence::Update— redirects the wrapped update's standard output (fd 1) to standard error or/dev/nullwhile it runs, then restores it, so a headless update never pollutes a stdio stream a parent process is monitoring (for example, a stdio-based MCP server).
The actual update source (GitHub, a custom server, etc.) is supplied by the
caller as any ReleaseUpdate implementation, e.g. one of self_update's
backends.
Installation
[]
= "0.1"
= "0.44"
Usage
Each wrapper follows self_update's builder convention: Update::configure()
returns an UpdateBuilder, and build() produces a
Box<dyn ReleaseUpdate>.
use ;
use github;
use ReleaseUpdate;
use Duration;
Composition order matters
Each wrapper can be used individually — you can wrap a backend with just
throttle::Update, restart::Update, or silence::Update. But if you combine
them, restart must always be the outermost layer:
backend → silence → throttle → restart
On a successful update, restart replaces the current process (exec on
Unix) or spawns the new binary and exits (Windows) — in both cases the call
never returns on success. If throttle were the outer wrapper, its
"record the check time" step would never run because the process would already
have been replaced.
silence must also stay inside restart: it restores fd 1 only when its
own update call returns, so if it wrapped restart the exec would happen
while stdout is still diverted and the re-executed process would inherit the
redirected fd 1. Placing it closest to the backend also scopes the redirect to
exactly the update's noisy work.
Wrappers
throttle::Update
Why: Update checks over the network waste bandwidth, battery, and server resources.
Without throttling, a user launching the same CLI multiple times in a short window
would hammer the update endpoint every single time. Many SaaS providers (GitHub,
GitLab, etc.) also enforce API rate limits — unthrottled checks can burn through
those quotas quickly, causing legitimate API calls to fail with 403 errors.
The throttle wrapper records the last check time and returns UpToDate immediately
when the configured window hasn't elapsed, avoiding unnecessary network calls and
protecting against rate-limit exhaustion.
| Builder method | Description |
|---|---|
release_update(Box<dyn ReleaseUpdate>) |
The wrapped updater. Required. |
throttle_window(Duration) |
Minimum interval between checks. Default: 15 minutes. |
build() |
Returns Result<Box<dyn ReleaseUpdate>>; errors if release_update is missing. |
When update() is called, the check is skipped (returning UpToDate) if the
throttle file was modified within throttle_window. Otherwise the wrapped
updater runs and the throttle file is touched. The file lives at
<temp_dir>/<bin_name>.throttle, where bin_name comes from the wrapped
updater.
restart::Update
Why: After installing a new binary, the old process image is still loaded in memory.
On Unix, calling exec replaces it in-place; on Windows, the caller must spawn the
new binary and exit. The restart wrapper handles this transparently, re-executing the
process with the same arguments so the user never has to manually re-launch.
Windows note: On Windows the wrapper spawns the new binary and exits the old one, so the original process ID is lost. If a parent process tracks your application by PID (e.g. a service manager or supervisor), it must be prepared to handle the PID disappearing and a new one appearing after an update.
| Builder method | Description |
|---|---|
release_update(Box<dyn ReleaseUpdate>) |
The wrapped updater. Required. |
guard_env(&str) |
Guard environment variable used to prevent restart loops. Default: RESTART_GUARD. |
build() |
Returns Result<Box<dyn ReleaseUpdate>>; errors if release_update is missing. |
When update() returns Updated, the process restarts into the freshly
installed binary, forwarding the original arguments and setting the guard
variable. On the re-executed run the guard is detected and the check is
skipped, so the update happens at most once per launch. Restart is supported
on both Unix (via exec) and Windows (spawn-and-exit, propagating the child's
exit code).
silence::Update
Why: A fully headless updater often runs beneath a parent process that is monitoring the child's standard I/O for other purposes. The canonical case is a stdio-based MCP server, where fd 1 carries the JSON-RPC protocol stream — any stray byte a backend prints there (prompts, progress, status lines) corrupts the protocol. The silence wrapper redirects fd 1 for the duration of the update and restores it before returning, so the update stays invisible on stdout.
| Builder method | Description |
|---|---|
release_update(Box<dyn ReleaseUpdate>) |
The wrapped updater. Required. |
sink(silence::Sink) |
Where fd 1 is redirected while the update runs: Sink::Stderr (fold into fd 2, the default) or Sink::Null (discard via /dev/null). |
build() |
Returns Result<Box<dyn ReleaseUpdate>>; errors if release_update is missing. |
Before delegating to the wrapped updater, stdout is pointed at the chosen sink; the original is restored when the call returns, on both success and error. Because the redirect is applied below the language runtime, it also captures output from spawned child processes and native code — not just the current process's buffered stdout.
- Unix: fd 1 is duplicated and swapped via
dup/dup2. - Windows: the process's standard-output handle is swapped via
GetStdHandle/SetStdHandle(pointed at theNULdevice or the standard-error handle). Rust'sstdre-queriesGetStdHandleon every write, so its output honors the swap. - Other platforms: redirection is unsupported, so the wrapped update runs unchanged.
Metadata methods (no_confirm, show_output, show_download_progress, and
the rest) delegate to the wrapped updater. Since a backend consults its own
configuration when it runs, overriding those on the wrapper would not affect an
arbitrarily deep composition, so silence relies solely on the fd redirect to
keep the update quiet.
License
Apache-2.0 — see LICENSE for details.