From b1776daa38f91b9147120457044c63b330caed81 Mon Sep 17 00:00:00 2001 From: Anten Skrabec Date: Wed, 29 Jul 2026 14:45:54 -0600 Subject: [PATCH] refactor: clean up headless client convergence and supervisor Replace long argument lists in convergence functions with ConvergeContext struct, eliminating 6 clippy::too_many_arguments suppressions. Fix ensure_fika_headless to write to mod_overlay() instead of headless_base, keeping the base install directory unmodified and making Fika.Headless visible through the standard overlay layering. Exit watchers now read restart_policy, max_restart_attempts, and restart_backoff_cap from config on each loop iteration instead of caching them at spawn time. Config changes (e.g. switching restart policy or adjusting backoff) take effect immediately without needing to restart the supervisor. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 +- src/cli/serve.rs | 22 +++--- src/client/converge.rs | 157 +++++++++++++++++---------------------- src/client/supervisor.rs | 133 ++++++++++++++------------------- src/headless/service.rs | 75 ++++++++----------- 5 files changed, 167 insertions(+), 222 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3c25347..fabc715 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,7 +91,7 @@ Single Rust binary — the CLI and actix-web server share the same codebase. The - **`src/tls.rs`** — TLS certificate loading/generation for the HTTPS proxy. - **`src/dirs.rs`** — `QumaDirs` struct: resolves the directory layout (`spt-server/`, `headless/`, `overlay/`) with legacy flat-layout migration. - **`src/invite.rs`** — Invite code generation and expiry parsing. -- **`src/client/`** — Fika headless client management. `supervisor.rs` runs the convergence loop, `converge.rs` handles container creation/scaling/overlay setup. Exit watchers cache restart policy/backoff values at spawn time (config changes to those require supervisor restart). +- **`src/client/`** — Fika headless client management. `supervisor.rs` runs the convergence loop, `converge.rs` handles container creation/scaling/overlay setup. Exit watchers read restart policy/backoff values from config on each iteration, so config changes take effect without restarting the supervisor. - **`src/headless/`** — Headless client service layer. `service.rs` defines `HeadlessService` (scaling, lifecycle actions, status). `operations.rs` provides `OperationTracker` for async operation tracking. `error.rs` defines `HeadlessError`. - **`src/spt/headless.rs`** — SPT server API types for headless client queries. - **`src/spt/game_data.rs`** — Loads quest/trader/hideout metadata from SPT data files for profile display. diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 4b47f29..c3b2ca0 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -145,19 +145,17 @@ pub async fn run(bind: Option<&str>, port: Option, cli: &Cli) -> Result<()> "Running initial convergence for {} headless client(s)", headless_config.client_count() ); - if let Err(e) = crate::client::converge::converge( - container_mgr_arc, + let ctx = crate::client::converge::ConvergeContext { + container_mgr: container_mgr_arc, headless_config, - &config, - &dirs, - &spt_client, - &forge, - &spt_info.spt_version, - Arc::clone(&converging), - &db_arc, - ) - .await - { + config: &config, + dirs: &dirs, + spt_client: &spt_client, + forge: &forge, + converging: &converging, + db: &db_arc, + }; + if let Err(e) = crate::client::converge::converge(&ctx).await { tracing::error!(err = %e, "Initial convergence failed"); } } diff --git a/src/client/converge.rs b/src/client/converge.rs index caa8c02..e8298a5 100644 --- a/src/client/converge.rs +++ b/src/client/converge.rs @@ -26,6 +26,18 @@ impl Drop for ConvergingGuard { } } +/// Shared context for convergence operations, avoiding long argument lists. +pub struct ConvergeContext<'a> { + pub container_mgr: &'a ContainerManager, + pub headless_config: &'a HeadlessConfig, + pub config: &'a Config, + pub dirs: &'a QumaDirs, + pub spt_client: &'a SptClient, + pub forge: &'a ForgeClient, + pub converging: &'a Arc, + pub db: &'a Arc>, +} + /// Label key for marking containers as managed by quartermaster pub const MANAGED_BY_LABEL: &str = "quma.managed-by"; pub const MANAGED_BY_VALUE: &str = "quartermaster-clients"; @@ -768,28 +780,24 @@ pub fn find_name_conflicts( /// /// The `converging` flag is an Arc that prevents concurrent convergence /// operations and signals to the supervisor that state is in flux. -#[allow(clippy::too_many_arguments)] #[allow(deprecated)] -pub async fn converge( - container_mgr: &ContainerManager, - headless_config: &HeadlessConfig, - config: &Config, - dirs: &QumaDirs, - spt_client: &SptClient, - forge: &ForgeClient, - _spt_version: &str, - converging: Arc, - db: &Arc>, -) -> Result<()> { +pub async fn converge(ctx: &ConvergeContext<'_>) -> Result<()> { // Set converging flag (atomic compare-exchange for race-free check-and-set) - if converging + if ctx + .converging .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .is_err() { bail!("Convergence already in progress"); } - let _guard = ConvergingGuard(converging.clone()); + let _guard = ConvergingGuard(ctx.converging.clone()); + + let container_mgr = ctx.container_mgr; + let headless_config = ctx.headless_config; + let dirs = ctx.dirs; + let forge = ctx.forge; + let db = ctx.db; // Legacy layout guard — overlay paths are empty in legacy mode if dirs.is_legacy() { @@ -929,7 +937,9 @@ pub async fn converge( // changes from install/update/remove are visible automatically. if desired_count > 0 { // Ensure Fika.Headless plugin is installed (GitHub-only, not on Forge). - ensure_fika_headless(forge, &dirs.headless_base).await?; + // Write to mod_overlay so it's visible to both SPT server and headless + // overlays without modifying the base headless install directory. + ensure_fika_headless(forge, &dirs.mod_overlay()).await?; } // Look up installed Fika version for Last Version overlay @@ -945,11 +955,7 @@ pub async fn converge( // Scale up or down if current_count < desired_count { ensure_clients( - container_mgr, - headless_config, - config, - dirs, - spt_client, + ctx, current_count, desired_count, ntsync_available, @@ -958,16 +964,7 @@ pub async fn converge( ) .await?; } else if current_count > desired_count { - remove_excess_clients( - container_mgr, - headless_config, - config, - dirs, - spt_client, - current_count, - desired_count, - ) - .await?; + remove_excess_clients(ctx, current_count, desired_count).await?; } else { info!("Already at desired count ({desired_count}), checking for overlay updates"); } @@ -1031,10 +1028,7 @@ pub async fn converge( .with_context(|| format!("failed to remove client {i} for NUMA reconciliation"))?; create_client_container( - container_mgr, - headless_config, - config, - dirs, + ctx, i, profile_id, ntsync_available, @@ -1149,13 +1143,8 @@ async fn warn_active_players(spt_client: &SptClient, reason: &str) { } /// Ensure containers exist from current_count up to desired_count. -#[allow(clippy::too_many_arguments)] async fn ensure_clients( - container_mgr: &ContainerManager, - headless_config: &HeadlessConfig, - config: &Config, - dirs: &QumaDirs, - spt_client: &SptClient, + ctx: &ConvergeContext<'_>, current_count: u32, desired_count: u32, ntsync_available: bool, @@ -1168,46 +1157,47 @@ async fn ensure_clients( { // ponytail: no fika_config_lock here — convergence is serialized by the converging flag, // and the config UI save handler is the only other writer. Race window is narrow. - let fika_path = crate::fika::config::fika_config_path(dirs); + let fika_path = crate::fika::config::fika_config_path(ctx.dirs); let cst = crate::fika::config::read_fika_cst(&fika_path)?; crate::fika::config::set_headless_amount(&cst, desired_count); crate::fika::config::write_fika_cst(&cst, &fika_path)?; } // 2. Restart SPT server to pick up new headless count and generate profiles - let container = config + let container = ctx + .config .server_container .as_deref() .expect("server_container validated by HeadlessConfig::validate"); - warn_active_players(spt_client, "scaling up headless clients").await; + warn_active_players(ctx.spt_client, "scaling up headless clients").await; info!("Stopping SPT server"); - container_mgr + ctx.container_mgr .stop(container) .await .context("failed to stop SPT server for headless config update")?; info!("Starting SPT server"); - container_mgr + ctx.container_mgr .start(container) .await .context("failed to start SPT server after headless config update")?; info!( "Waiting for SPT server to become ready (timeout: {}s)", - headless_config.server_ready_timeout + ctx.headless_config.server_ready_timeout ); - if !await_server_ready(spt_client, headless_config.server_ready_timeout).await { + if !await_server_ready(ctx.spt_client, ctx.headless_config.server_ready_timeout).await { bail!( "SPT server did not become ready within {}s after restart. \ Headless clients will not be started against a half-initialized server. \ Increase headless.server_ready_timeout if your server needs more time to load.", - headless_config.server_ready_timeout + ctx.headless_config.server_ready_timeout ); } // 3. Restart existing containers so they reconnect to the fresh server - restart_running_clients(container_mgr, current_count).await?; + restart_running_clients(ctx.container_mgr, current_count).await?; // 4. Discover available profiles for assignment // Headless profiles are created by the SPT server when it starts with Fika's @@ -1215,28 +1205,19 @@ async fn ensure_clients( // since headless amount was increased), containers are created without PROFILE_ID // and will need a re-scale after the server generates the profiles. let new_count = desired_count - current_count; - let managed = container_mgr + let managed = ctx + .container_mgr .detect_containers_by_label(MANAGED_BY_LABEL, MANAGED_BY_VALUE) .await .unwrap_or_default(); let profile_assignments = - select_profiles_for_assignment(container_mgr, dirs, &managed, new_count).await; + select_profiles_for_assignment(ctx.container_mgr, ctx.dirs, &managed, new_count).await; // 5. Create containers for new clients for (offset, profile_id) in profile_assignments.into_iter().enumerate() { let i = current_count + 1 + offset as u32; - create_client_container( - container_mgr, - headless_config, - config, - dirs, - i, - profile_id, - ntsync_available, - topology, - fika_version, - ) - .await?; + create_client_container(ctx, i, profile_id, ntsync_available, topology, fika_version) + .await?; } Ok(()) @@ -1247,11 +1228,7 @@ async fn ensure_clients( /// In-raid checks are now handled by the CLI `headless delete` command before calling /// converge, so this function simply stops and removes excess containers. async fn remove_excess_clients( - container_mgr: &ContainerManager, - headless_config: &HeadlessConfig, - config: &Config, - dirs: &QumaDirs, - spt_client: &SptClient, + ctx: &ConvergeContext<'_>, current_count: u32, desired_count: u32, ) -> Result<()> { @@ -1263,21 +1240,21 @@ async fn remove_excess_clients( info!("Removing container {name}"); // Stop first if running - if container_mgr.is_running(&name).await? { - container_mgr.stop(&name).await?; + if ctx.container_mgr.is_running(&name).await? { + ctx.container_mgr.stop(&name).await?; } - container_mgr.remove_container(&name).await?; + ctx.container_mgr.remove_container(&name).await?; } // Clean up overlay mounts and directories for removed clients for i in (desired_count + 1)..=current_count { // Unmount the overlay before removing the directory tree - if let Err(e) = dirs.headless_overlay_mount(i).unmount() { + if let Err(e) = ctx.dirs.headless_overlay_mount(i).unmount() { warn!("Failed to unmount overlay for client {i}: {e}"); } - let overlay = dirs.headless_overlay(i); + let overlay = ctx.dirs.headless_overlay(i); if overlay.exists() { if let Err(e) = std::fs::remove_dir_all(&overlay) { warn!("Failed to clean overlay dir for client {i}: {e}"); @@ -1291,25 +1268,26 @@ async fn remove_excess_clients( { // ponytail: no fika_config_lock here — convergence is serialized by the converging flag, // and the config UI save handler is the only other writer. Race window is narrow. - let fika_path = crate::fika::config::fika_config_path(dirs); + let fika_path = crate::fika::config::fika_config_path(ctx.dirs); let cst = crate::fika::config::read_fika_cst(&fika_path)?; crate::fika::config::set_headless_amount(&cst, desired_count); crate::fika::config::write_fika_cst(&cst, &fika_path)?; } // 3. Restart SPT server to deregister removed clients - let container = config + let container = ctx + .config .server_container .as_deref() .expect("server_container validated by HeadlessConfig::validate"); - warn_active_players(spt_client, "scaling down headless clients").await; + warn_active_players(ctx.spt_client, "scaling down headless clients").await; info!("Restarting SPT server to deregister removed headless clients"); - container_mgr + ctx.container_mgr .stop(container) .await .context("failed to stop SPT server for client deregistration")?; - container_mgr + ctx.container_mgr .start(container) .await .context("failed to start SPT server after client deregistration")?; @@ -1317,19 +1295,19 @@ async fn remove_excess_clients( // 4. Wait for server readiness before restarting remaining clients info!( "Waiting for SPT server to become ready (timeout: {}s)", - headless_config.server_ready_timeout + ctx.headless_config.server_ready_timeout ); - if !await_server_ready(spt_client, headless_config.server_ready_timeout).await { + if !await_server_ready(ctx.spt_client, ctx.headless_config.server_ready_timeout).await { bail!( "SPT server did not become ready within {}s after restart. \ Remaining clients will not be restarted against a half-initialized server. \ Increase headless.server_ready_timeout if your server needs more time to load.", - headless_config.server_ready_timeout + ctx.headless_config.server_ready_timeout ); } // 5. Restart remaining clients so they reconnect to the fresh server - restart_running_clients(container_mgr, desired_count).await?; + restart_running_clients(ctx.container_mgr, desired_count).await?; Ok(()) } @@ -1399,19 +1377,18 @@ fn resolve_numa_cpuset( /// The game directory is mounted via Podman's overlay mount (`:O`), giving each /// container a private writable layer over the shared install_dir. A first-boot /// flow waits for Fika to generate its config before patching and restarting. -#[allow(clippy::too_many_arguments)] -#[allow(deprecated)] async fn create_client_container( - container_mgr: &ContainerManager, - headless_config: &HeadlessConfig, - config: &Config, - dirs: &QumaDirs, + ctx: &ConvergeContext<'_>, index: u32, profile_id: Option, ntsync_available: bool, topology: &NumaTopology, fika_version: Option<&str>, ) -> Result<()> { + let headless_config = ctx.headless_config; + let dirs = ctx.dirs; + let container_mgr = ctx.container_mgr; + let name = client_container_name(index); let overlay_dir = dirs.headless_overlay(index); @@ -1478,12 +1455,12 @@ async fn create_client_container( // Route through quma's HTTPS proxy. // Host-networked containers are on the host's network stack directly, // so use 127.0.0.1 instead of host.containers.internal. - let proxy_host = match config.web_bind.as_str() { + let proxy_host = match ctx.config.web_bind.as_str() { "0.0.0.0" | "127.0.0.1" | "localhost" | "" => "127.0.0.1", other => other, }; env.push(("SERVER_URL".to_string(), proxy_host.to_string())); - env.push(("SERVER_PORT".to_string(), config.web_port.to_string())); + env.push(("SERVER_PORT".to_string(), ctx.config.web_port.to_string())); env.push(("ESYNC".to_string(), headless_config.esync.to_string())); env.push(("FSYNC".to_string(), headless_config.fsync.to_string())); diff --git a/src/client/supervisor.rs b/src/client/supervisor.rs index 9d2521e..84093ca 100644 --- a/src/client/supervisor.rs +++ b/src/client/supervisor.rs @@ -17,6 +17,18 @@ use crate::container::ContainerManager; use crate::spt::headless::{EHeadlessStatus, GetHeadlessesResponse}; use crate::spt::server::SptClient; +/// Shared context for exit watchers, avoiding long argument lists. +/// Config is read dynamically on each loop iteration so restart policy +/// changes take effect without restarting the supervisor. +struct ExitWatcherCtx { + container_mgr: ContainerManager, + state: Arc>>, + watcher_handles: Arc>>, + converging: Arc, + cancel_token: CancellationToken, + config: Arc>, +} + struct RestartingGuard { state: Arc>>, index: u32, @@ -211,21 +223,8 @@ impl ClientSupervisor { if state.container_status == ContainerStatus::Running { let has_watcher = self.watcher_handles.read().await.contains_key(&state.index); if !has_watcher { - ClientSupervisor::spawn_exit_watcher( - self.container_mgr.clone(), - Arc::clone(&self.state), - Arc::clone(&self.watcher_handles), - state.index, - state.container_name.clone(), - headless_config.restart_policy.clone(), - headless_config.max_restart_attempts, - headless_config.restart_backoff_cap, - self.converging.clone(), - self.cancel_token.clone(), - Arc::clone(&self.config), - Arc::clone(&self.db), - ) - .await; + self.spawn_exit_watcher(state.index, state.container_name.clone()) + .await; } } } @@ -407,86 +406,66 @@ impl ClientSupervisor { Ok(state) } - #[allow(clippy::too_many_arguments)] - #[allow(clippy::too_many_arguments)] - async fn spawn_exit_watcher( - container_mgr: ContainerManager, - state: Arc>>, - watcher_handles: Arc>>, - index: u32, - container_name: String, - restart_policy: RestartPolicy, - max_restart_attempts: u32, - backoff_cap: u64, - converging: Arc, - cancel_token: CancellationToken, - config: Arc>, - db: Arc>, - ) { + async fn spawn_exit_watcher(&self, index: u32, container_name: String) { + let ctx = ExitWatcherCtx { + container_mgr: self.container_mgr.clone(), + state: Arc::clone(&self.state), + watcher_handles: Arc::clone(&self.watcher_handles), + converging: self.converging.clone(), + cancel_token: self.cancel_token.clone(), + config: Arc::clone(&self.config), + }; + // Child token: cancelled when either the supervisor shuts down // (cancel_token) or this specific watcher is replaced/removed. - let watcher_cancel = cancel_token.child_token(); + let watcher_cancel = ctx.cancel_token.child_token(); // Register this watcher, cancelling any previous one for the same index { - let mut handles = watcher_handles.write().await; + let mut handles = ctx.watcher_handles.write().await; if let Some(old) = handles.insert(index, watcher_cancel.clone()) { old.cancel(); } } - let watcher_cancel_clone = watcher_cancel.clone(); - let container_mgr_clone = container_mgr.clone(); - let state_clone = Arc::clone(&state); - let watcher_handles_clone = Arc::clone(&watcher_handles); - tokio::spawn(async move { - exit_watcher_loop( - container_mgr_clone, - state_clone, - watcher_handles_clone, - index, - container_name, - restart_policy, - max_restart_attempts, - backoff_cap, - converging, - watcher_cancel_clone, - config, - db, - ) - .await; + exit_watcher_loop(ctx, index, container_name, watcher_cancel).await; }); } } -#[allow(clippy::too_many_arguments)] async fn exit_watcher_loop( - container_mgr: ContainerManager, - state: Arc>>, - watcher_handles: Arc>>, + ctx: ExitWatcherCtx, index: u32, container_name: String, - restart_policy: RestartPolicy, - max_restart_attempts: u32, - backoff_cap: u64, - converging: Arc, cancel_token: CancellationToken, - _config: Arc>, - _db: Arc>, ) { let mut retry_delay = Duration::from_secs(1); let max_retry_delay = Duration::from_secs(30); loop { + // Read restart config fresh each iteration so changes take effect + // without restarting the supervisor. + let (restart_policy, max_restart_attempts, backoff_cap) = { + let cfg = ctx.config.read(); + match cfg.headless { + Some(ref hc) => ( + hc.restart_policy.clone(), + hc.max_restart_attempts, + hc.restart_backoff_cap, + ), + None => (RestartPolicy::Auto, 5, 300), + } + }; + // Watch the container for exit - let mut stream = container_mgr.wait_container(&container_name); + let mut stream = ctx.container_mgr.wait_container(&container_name); let wait_result = tokio::select! { _ = cancel_token.cancelled() => { tracing::debug!(container = %container_name, "Exit watcher cancelled"); // Clean up our handle - watcher_handles.write().await.remove(&index); + ctx.watcher_handles.write().await.remove(&index); return; } result = stream.next() => result, @@ -539,7 +518,7 @@ async fn exit_watcher_loop( container = %container_name, "Exit watcher stream ended unexpectedly" ); - watcher_handles.write().await.remove(&index); + ctx.watcher_handles.write().await.remove(&index); return; } }; @@ -547,7 +526,7 @@ async fn exit_watcher_loop( // Check for OOM kill: the kernel kills the game process but the // entrypoint exits cleanly (code 0). Without this check, OOM kills // look like normal restarts and never trigger backoff. - if is_clean_exit && container_mgr.was_oom_killed(&container_name).await { + if is_clean_exit && ctx.container_mgr.was_oom_killed(&container_name).await { is_clean_exit = false; tracing::warn!( container = %container_name, @@ -557,7 +536,7 @@ async fn exit_watcher_loop( // Update state with exit info let should_restart = { - let mut state_lock = state.write().await; + let mut state_lock = ctx.state.write().await; if let Some(s) = state_lock.iter_mut().find(|s| s.index == index) { s.container_status = ContainerStatus::Stopped; s.health = ClientHealth::Down; @@ -583,7 +562,7 @@ async fn exit_watcher_loop( } }; - if converging.load(Ordering::Relaxed) { + if ctx.converging.load(Ordering::Relaxed) { tracing::debug!( container = %container_name, "Skipping exit-watcher restart (convergence in progress)" @@ -595,7 +574,7 @@ async fn exit_watcher_loop( if !should_restart { let reason = { - let state_lock = state.read().await; + let state_lock = ctx.state.read().await; if let Some(s) = state_lock.iter().find(|s| s.index == index) { if s.manually_stopped { "manually stopped" @@ -611,13 +590,13 @@ async fn exit_watcher_loop( exit_code, "Not restarting ({reason})" ); - watcher_handles.write().await.remove(&index); + ctx.watcher_handles.write().await.remove(&index); return; } // Mark as restarting { - let mut state_lock = state.write().await; + let mut state_lock = ctx.state.write().await; if let Some(s) = state_lock.iter_mut().find(|s| s.index == index) { s.restarting = true; } @@ -625,7 +604,7 @@ async fn exit_watcher_loop( // Get failure count for backoff calculation let failures = { - let state_lock = state.read().await; + let state_lock = ctx.state.read().await; state_lock .iter() .find(|s| s.index == index) @@ -634,7 +613,7 @@ async fn exit_watcher_loop( }; let _guard = RestartingGuard { - state: state.clone(), + state: ctx.state.clone(), index, }; @@ -652,9 +631,9 @@ async fn exit_watcher_loop( // Start the already-stopped container (don't use restart() — // calling stop() on an exited container errors with 304) - match container_mgr.start(&container_name).await { + match ctx.container_mgr.start(&container_name).await { Ok(()) => { - let mut state_lock = state.write().await; + let mut state_lock = ctx.state.write().await; if let Some(s) = state_lock.iter_mut().find(|s| s.index == index) { s.restart_count += 1; s.last_restart = Some(Utc::now()); @@ -674,7 +653,7 @@ async fn exit_watcher_loop( err = %e, "Failed to restart" ); - watcher_handles.write().await.remove(&index); + ctx.watcher_handles.write().await.remove(&index); return; } } diff --git a/src/headless/service.rs b/src/headless/service.rs index 43329c1..b34c3f7 100644 --- a/src/headless/service.rs +++ b/src/headless/service.rs @@ -125,26 +125,23 @@ impl HeadlessService { let dirs = Arc::clone(&self.dirs); let spt_client = self.spt_client(); let forge = self.forge.clone(); - let spt_version = ""; let converging = Arc::clone(&self.converging); let db = Arc::clone(&self.db); let ops = self.operations.clone(); let events = self.events.clone(); tokio::spawn(async move { - match crate::client::converge::converge( - &mgr, - &headless_config, - &config, - &dirs, - &spt_client, - &forge, - spt_version, - converging, - &db, - ) - .await - { + let ctx = crate::client::converge::ConvergeContext { + container_mgr: &mgr, + headless_config: &headless_config, + config: &config, + dirs: &dirs, + spt_client: &spt_client, + forge: &forge, + converging: &converging, + db: &db, + }; + match crate::client::converge::converge(&ctx).await { Ok(()) => { ops.complete(&op_id); let _ = events.send(ServerEvent::HeadlessChanged); @@ -554,7 +551,6 @@ impl HeadlessService { let dirs = Arc::clone(&self.dirs); let spt_client = self.spt_client(); let forge = self.forge.clone(); - let spt_version = ""; let converging = Arc::clone(&self.converging); let db = Arc::clone(&self.db); let ops = self.operations.clone(); @@ -599,19 +595,17 @@ impl HeadlessService { let _ = std::fs::remove_dir_all(&overlay); } - match crate::client::converge::converge( - &mgr, - &updated_config, - &config, - &dirs, - &spt_client, - &forge, - spt_version, - converging, - &db, - ) - .await - { + let ctx = crate::client::converge::ConvergeContext { + container_mgr: &mgr, + headless_config: &updated_config, + config: &config, + dirs: &dirs, + spt_client: &spt_client, + forge: &forge, + converging: &converging, + db: &db, + }; + match crate::client::converge::converge(&ctx).await { Ok(()) => { ops.complete(&op_id); let _ = events.send(ServerEvent::HeadlessChanged); @@ -664,7 +658,6 @@ impl HeadlessService { let dirs = Arc::clone(&self.dirs); let spt_client = self.spt_client(); let forge = self.forge.clone(); - let spt_version = ""; let converging = Arc::clone(&self.converging); let db = Arc::clone(&self.db); let ops = self.operations.clone(); @@ -699,19 +692,17 @@ impl HeadlessService { converging.store(false, std::sync::atomic::Ordering::Release); - match crate::client::converge::converge( - &mgr, - &headless_config, - &config, - &dirs, - &spt_client, - &forge, - spt_version, - converging, - &db, - ) - .await - { + let ctx = crate::client::converge::ConvergeContext { + container_mgr: &mgr, + headless_config: &headless_config, + config: &config, + dirs: &dirs, + spt_client: &spt_client, + forge: &forge, + converging: &converging, + db: &db, + }; + match crate::client::converge::converge(&ctx).await { Ok(()) => { ops.complete(&op_id); let _ = events.send(ServerEvent::HeadlessChanged);