generated from eblume/project-template
C2(hephd-self-update): impl --self-update opt-in flag + config plumbing
Add --self-update (default off) and --self-update-interval-secs to the
hephd CLI, a SelfUpdateConfig (Some => enabled), and thread it into the
Daemon (with_self_update) for every mode. spawn_self_update_loop()
currently just announces the mode at startup ('self-update enabled')
so the opt-in is observable; the poll/apply cycle is wired in later
leaves. Omitting the flag leaves behaviour unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4a0094f955
commit
f6bcd50684
3 changed files with 81 additions and 4 deletions
|
|
@ -17,8 +17,8 @@ use tokio::net::{TcpListener, UnixListener};
|
|||
|
||||
use heph_core::LocalStore;
|
||||
use hephd::{
|
||||
default_db_path, default_socket_path, sync, Daemon, KeyringTokenStore, LockGuard, RemoteStore,
|
||||
SystemClock, TokenStore,
|
||||
default_db_path, default_socket_path, selfupdate::SelfUpdateConfig, sync, Daemon,
|
||||
KeyringTokenStore, LockGuard, RemoteStore, SystemClock, TokenStore,
|
||||
};
|
||||
|
||||
/// How often a spoke background-syncs with its hub.
|
||||
|
|
@ -77,6 +77,16 @@ struct Cli {
|
|||
/// --oidc-issuer, the device attaches a cached bearer token to hub requests.
|
||||
#[arg(long)]
|
||||
oidc_client_id: Option<String>,
|
||||
|
||||
/// Opt-in (default off): periodically poll the forge for a newer release and
|
||||
/// auto-update this daemon. Off unless this flag is given.
|
||||
#[arg(long)]
|
||||
self_update: bool,
|
||||
|
||||
/// Override the self-update poll interval, in seconds (default: 6h). Only
|
||||
/// meaningful with --self-update.
|
||||
#[arg(long)]
|
||||
self_update_interval_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// Build the spoke/client token source: a keyring store keyed by `account` (the
|
||||
|
|
@ -112,6 +122,11 @@ async fn main() -> Result<()> {
|
|||
.with_context(|| format!("creating socket dir {}", parent.display()))?;
|
||||
}
|
||||
|
||||
// Opt-in self-update (default off): `Some` only when `--self-update` is set.
|
||||
let self_update = cli
|
||||
.self_update
|
||||
.then(|| SelfUpdateConfig::new(cli.self_update_interval_secs.map(Duration::from_secs)));
|
||||
|
||||
// Build the daemon for the chosen mode. `local`/`server` own the file (and
|
||||
// hold its lock for the process's life); `client` keeps no replica.
|
||||
let (_lock, daemon) = match cli.mode {
|
||||
|
|
@ -131,7 +146,10 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
None => RemoteStore::new(&server_url),
|
||||
};
|
||||
(None, Daemon::new(store))
|
||||
(
|
||||
None,
|
||||
Daemon::new(store).with_self_update(self_update.clone()),
|
||||
)
|
||||
}
|
||||
Mode::Local | Mode::Server => {
|
||||
let db = cli.db.clone().unwrap_or_else(default_db_path);
|
||||
|
|
@ -147,7 +165,8 @@ async fn main() -> Result<()> {
|
|||
});
|
||||
let daemon = Daemon::new(store)
|
||||
.with_hub(cli.hub_url.clone())
|
||||
.with_spoke_auth(spoke);
|
||||
.with_spoke_auth(spoke)
|
||||
.with_self_update(self_update.clone());
|
||||
|
||||
// server mode: expose the hub HTTP endpoint over the same store.
|
||||
if cli.mode == Mode::Server {
|
||||
|
|
@ -190,6 +209,9 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
};
|
||||
|
||||
// Opt-in self-update poller (no-op unless --self-update); mode-agnostic.
|
||||
daemon.spawn_self_update_loop();
|
||||
|
||||
// Replace any stale socket from a previous run, then bind.
|
||||
if socket.exists() {
|
||||
std::fs::remove_file(&socket)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,30 @@
|
|||
//! The decision logic is pure and unit-tested; the network fetch is isolated so
|
||||
//! its JSON parsing can be tested against a sample body without a live forge.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
/// Default poll cadence when `--self-update` is on and no interval is given.
|
||||
pub const DEFAULT_INTERVAL: Duration = Duration::from_secs(6 * 60 * 60);
|
||||
|
||||
/// Configuration for the opt-in self-update mode. Its mere presence (the daemon
|
||||
/// holds an `Option<SelfUpdateConfig>`) means the mode is enabled; absent ⇒ off.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SelfUpdateConfig {
|
||||
/// How often to poll the forge for a newer release.
|
||||
pub interval: Duration,
|
||||
}
|
||||
|
||||
impl SelfUpdateConfig {
|
||||
/// Build a config, falling back to [`DEFAULT_INTERVAL`] when no override.
|
||||
pub fn new(interval: Option<Duration>) -> Self {
|
||||
Self {
|
||||
interval: interval.unwrap_or(DEFAULT_INTERVAL),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The forge releases feed for this project — the latest tagged release.
|
||||
/// Uses the SSH-canonical host (`forge.ops.eblu.me`); see the
|
||||
/// `service-env-forge-access` card for the cargo/forge host caveat.
|
||||
|
|
@ -67,6 +89,15 @@ pub async fn fetch_latest_tag(http: &reqwest::Client, url: &str) -> Result<Strin
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn config_defaults_interval_and_honors_override() {
|
||||
assert_eq!(SelfUpdateConfig::new(None).interval, DEFAULT_INTERVAL);
|
||||
assert_eq!(
|
||||
SelfUpdateConfig::new(Some(Duration::from_secs(900))).interval,
|
||||
Duration::from_secs(900)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_available_compares_ignoring_build_suffix_and_v_prefix() {
|
||||
// Running version carries a build-sha suffix; tags carry a `v`.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use heph_core::Store;
|
|||
|
||||
use crate::oauth::{self, TokenStore};
|
||||
use crate::rpc::{self, Request, Response, RpcError, INTERNAL_ERROR, PARSE_ERROR};
|
||||
use crate::selfupdate::SelfUpdateConfig;
|
||||
use crate::sync::{self, SharedStore};
|
||||
|
||||
/// How a spoke obtains the bearer token it presents to its hub (tech-spec §13).
|
||||
|
|
@ -40,6 +41,8 @@ struct Ctx {
|
|||
http: reqwest::Client,
|
||||
/// Token source for authenticated sync (None ⇒ unauthenticated hub).
|
||||
auth: Option<SpokeAuth>,
|
||||
/// Opt-in self-update config (`Some` ⇒ enabled, tech-spec self-update card).
|
||||
self_update: Option<SelfUpdateConfig>,
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
|
|
@ -76,6 +79,7 @@ impl Daemon {
|
|||
hub_url: None,
|
||||
http: reqwest::Client::new(),
|
||||
auth: None,
|
||||
self_update: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -100,12 +104,32 @@ impl Daemon {
|
|||
self
|
||||
}
|
||||
|
||||
/// Enable opt-in self-update with the given config (`None` ⇒ stays off).
|
||||
pub fn with_self_update(mut self, cfg: Option<SelfUpdateConfig>) -> Daemon {
|
||||
self.ctx.self_update = cfg;
|
||||
self
|
||||
}
|
||||
|
||||
/// The shared store handle, for code that needs to reach the same store the
|
||||
/// daemon serves (the hub HTTP router and background sync, tech-spec §6.1).
|
||||
pub fn store(&self) -> SharedStore {
|
||||
self.ctx.store.clone()
|
||||
}
|
||||
|
||||
/// If self-update is enabled, start its background poller. For now this only
|
||||
/// announces the mode at startup; the polling + apply cycle is wired in
|
||||
/// later self-update work. No-op when the mode is off.
|
||||
pub fn spawn_self_update_loop(&self) {
|
||||
let Some(cfg) = self.ctx.self_update.clone() else {
|
||||
return;
|
||||
};
|
||||
tracing::info!(
|
||||
interval_secs = cfg.interval.as_secs(),
|
||||
current = heph_core::VERSION,
|
||||
"self-update enabled"
|
||||
);
|
||||
}
|
||||
|
||||
/// If this is a spoke (`hub_url` set), spawn a background task that syncs the
|
||||
/// op-log with the hub every `interval` (attaching a bearer token when auth
|
||||
/// is configured). No-op otherwise.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue