# self-update-extras
Self-updating support for CLI binaries.
`self-update-extras` provides three small, composable wrappers around any type that
implements [`self_update`](https://crates.io/crates/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/null` while 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
```toml
[dependencies]
self-update-extras = "0.1"
self_update = "0.44"
```
## Usage
Each wrapper follows `self_update`'s builder convention: `Update::configure()`
returns an `UpdateBuilder`, and `build()` produces a
`Box<dyn ReleaseUpdate>`.
```rust
use self_update_extras::{restart, silence, throttle};
use self_update::backends::github;
use self_update::update::ReleaseUpdate;
use std::time::Duration;
fn check_for_update() -> Result<(), Box<dyn std::error::Error>> {
// 1. Any `ReleaseUpdate` implementation — here, self_update's GitHub backend.
let backend = github::Update::configure()
.repo_owner("my-org")
.repo_name("my-app")
.bin_name("my-app")
.current_version(self_update::cargo_crate_version!())
.no_confirm(true)
.build()?;
// 2. Silence the backend's output so it can't corrupt the stdio stream a
// parent process may be monitoring. Kept innermost, inside `restart`.
let quiet = silence::Update::configure()
.release_update(backend)
.sink(silence::Sink::Null)
.build()?;
// 3. Throttle how often the check actually runs.
let throttled = throttle::Update::configure()
.release_update(quiet)
.throttle_window(Duration::from_secs(15 * 60))
.build()?;
// 4. Restart into the new binary after a successful update.
// `restart` must be the OUTERMOST wrapper — see the note below.
let updater = restart::Update::configure()
.release_update(throttled)
.guard_env("MY_APP_AUTO_UPDATED")
.build()?;
// Runs the check, respecting the throttle window and restart guard.
let status = updater.update()?;
println!("update status: {status:?}");
Ok(())
}
```
### 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:
```text
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.
| `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.
| `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.
| `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 the `NUL` device or the
standard-error handle). Rust's `std` re-queries `GetStdHandle` on 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](./LICENSE) for details.