Skip to content

Commit 4fecb99

Browse files
committed
feat: implement background auto-rediscovery worker for Docker services
1 parent 4468f77 commit 4fecb99

1 file changed

Lines changed: 96 additions & 9 deletions

File tree

core/src/main.rs

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,76 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
277277
}
278278
});
279279

280+
// Background auto-rediscovery worker — refreshes metadata for all previously-discovered
281+
// containers every 60 s. This ensures that after a `docker compose up --force-recreate`
282+
// (or any redeploy that recreates containers), the dashboard picks up the new container_id
283+
// and freshly-built log/start/stop commands automatically, without requiring a manual
284+
// "Discover" click or an app restart.
285+
let rediscover_state = state.clone();
286+
tokio::spawn(async move {
287+
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60));
288+
loop {
289+
interval.tick().await;
290+
291+
// Collect unique (host, ssh_user) pairs that have at least one discovered service.
292+
let hosts: Vec<(String, Option<String>)> = {
293+
let s = rediscover_state.services.read().await;
294+
let mut seen = std::collections::HashSet::new();
295+
let mut result = Vec::new();
296+
for svc in s.iter().filter(|x| x.discovered) {
297+
let key = (svc.host.clone(), svc.ssh_user.clone());
298+
if seen.insert(key.clone()) {
299+
result.push(key);
300+
}
301+
}
302+
result
303+
};
304+
305+
for (host, ssh_user) in hosts {
306+
// Only skip YAML-configured services; rediscover all previously-discovered ones.
307+
let static_ids: std::collections::HashSet<String> = {
308+
let s = rediscover_state.services.read().await;
309+
s.iter().filter(|x| !x.discovered).map(|x| x.id.clone()).collect()
310+
};
311+
312+
match discover::discover_docker_services(&host, ssh_user.as_deref(), &static_ids).await {
313+
Ok(found) => {
314+
let mut changed = false;
315+
{
316+
let mut s = rediscover_state.services.write().await;
317+
for svc in found {
318+
if let Some(existing) = s.iter_mut().find(|x| x.id == svc.id && x.discovered) {
319+
// Only overwrite (and flag as changed) if something meaningful differs.
320+
if existing.container_id != svc.container_id || existing.status != svc.status {
321+
*existing = svc;
322+
changed = true;
323+
}
324+
} else if !s.iter().any(|x| x.id == svc.id) {
325+
s.push(svc);
326+
changed = true;
327+
}
328+
}
329+
}
330+
if changed {
331+
let s = rediscover_state.services.read().await;
332+
let snapshot = json!({"type": "full_state", "services": &*s}).to_string();
333+
let _ = rediscover_state.broadcaster.send(snapshot);
334+
let host_services: Vec<_> = s
335+
.iter()
336+
.filter(|svc| svc.host == host && svc.discovered)
337+
.cloned()
338+
.collect();
339+
config::save_discovered_services(&host, &host_services);
340+
}
341+
}
342+
Err(e) => {
343+
tracing::warn!("auto-rediscovery failed for {}: {}", host, e);
344+
}
345+
}
346+
}
347+
}
348+
});
349+
280350
// Plugin directory (can be overridden with PLUGIN_DIR env var).
281351
// When deployed, plugins live in bin/ next to the executable.
282352
// When running from source (cargo run), fall back to plugins/bin relative to the workspace root.
@@ -552,23 +622,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
552622
Err(_) => return axum::Json(json!({"ok": false, "error": "invalid json — expected {host, ssh_user}"})),
553623
};
554624

555-
// Collect existing service ids so we don't duplicate
556-
let existing_ids: std::collections::HashSet<String> = {
625+
// Only skip YAML-configured (non-discovered) services to avoid overriding user
626+
// config. Previously-discovered services are always refreshed so that redeployed
627+
// containers (same name, new container ID) get up-to-date metadata — container_id,
628+
// log_cmd, start/stop/restart commands — without requiring an app restart.
629+
let static_ids: std::collections::HashSet<String> = {
557630
let s = state.services.read().await;
558-
s.iter().map(|x| x.id.clone()).collect()
631+
s.iter().filter(|x| !x.discovered).map(|x| x.id.clone()).collect()
559632
};
560633

561634
match discover::discover_docker_services(
562635
&body.host,
563636
body.ssh_user.as_deref(),
564-
&existing_ids,
637+
&static_ids,
565638
).await {
566-
Ok(new_services) => {
567-
let count = new_services.len();
568-
// Merge into state
639+
Ok(found_services) => {
640+
let mut new_count = 0usize;
641+
let mut updated_count = 0usize;
569642
{
570643
let mut s = state.services.write().await;
571-
s.extend(new_services.clone());
644+
for svc in found_services.clone() {
645+
if let Some(existing) = s.iter_mut().find(|x| x.id == svc.id && x.discovered) {
646+
// Refresh the existing discovered service with up-to-date metadata
647+
*existing = svc;
648+
updated_count += 1;
649+
} else if !s.iter().any(|x| x.id == svc.id) {
650+
s.push(svc);
651+
new_count += 1;
652+
}
653+
}
572654
}
573655
// Broadcast updated full state
574656
{
@@ -586,7 +668,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
586668
.collect();
587669
config::save_discovered_services(&body.host, &host_services);
588670
}
589-
axum::Json(json!({"ok": true, "discovered": count, "services": new_services}))
671+
axum::Json(json!({
672+
"ok": true,
673+
"discovered": new_count,
674+
"updated": updated_count,
675+
"services": found_services
676+
}))
590677
}
591678
Err(e) => axum::Json(json!({"ok": false, "error": format!("{}", e)})),
592679
}

0 commit comments

Comments
 (0)