Fix update looping and nmcli duplicate profiles
This commit is contained in:
parent
586bc3a285
commit
ef77a02e77
2 changed files with 111 additions and 18 deletions
92
src/nm.rs
92
src/nm.rs
|
|
@ -240,6 +240,40 @@ fn enforce_dns(uuid: &str, iface: &str, dns: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return the name of the first saved NM connection profile whose name is
|
||||
/// either exactly `ssid` or `ssid N` (NM's numbered-duplicate convention).
|
||||
/// Returns `None` if no such profile exists.
|
||||
fn first_profile_for_ssid(ssid: &str) -> Option<String> {
|
||||
let o = run(
|
||||
"nmcli",
|
||||
&["-t", "-f", "NAME,TYPE", "connection", "show"],
|
||||
Duration::from_secs(8),
|
||||
);
|
||||
if !o.success {
|
||||
return None;
|
||||
}
|
||||
let mut fallback: Option<String> = None;
|
||||
for line in o.stdout.lines() {
|
||||
let parts: Vec<&str> = line.splitn(2, ':').collect();
|
||||
if parts.len() < 2 || !parts[1].contains("wireless") {
|
||||
continue;
|
||||
}
|
||||
let name = unescape(parts[0]);
|
||||
if name == ssid {
|
||||
return Some(name);
|
||||
}
|
||||
if fallback.is_none() {
|
||||
if let Some(suffix) = name.strip_prefix(ssid) {
|
||||
let s = suffix.trim();
|
||||
if !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()) {
|
||||
fallback = Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fallback
|
||||
}
|
||||
|
||||
/// Connect to a network and pin DNS. Returns true only if associated.
|
||||
pub fn connect(iface: &str, net: &NetworkDef, wait: u32, dns: &str) -> bool {
|
||||
connect_verbose(iface, net, wait, dns).is_ok()
|
||||
|
|
@ -247,12 +281,62 @@ pub fn connect(iface: &str, net: &NetworkDef, wait: u32, dns: &str) -> bool {
|
|||
|
||||
/// Connect to a network and pin DNS. Returns the nmcli error on failure.
|
||||
///
|
||||
/// Uses `nmcli device wifi connect ... password <psk>` so the provided PSK
|
||||
/// always takes effect, even when NM already has a stale saved profile for
|
||||
/// the same SSID. The PSK is briefly visible in /proc/<pid>/cmdline, which is
|
||||
/// an acceptable trade-off for a personal desktop tool.
|
||||
/// Reuses an existing saved profile for the SSID when one exists (updating its
|
||||
/// PSK) so that repeated connections do not accumulate numbered duplicates in
|
||||
/// NetworkManager ("NCC", "NCC 1", "NCC 2", …). Falls back to
|
||||
/// `nmcli device wifi connect` — which creates a new profile — only when no
|
||||
/// saved profile is found.
|
||||
pub fn connect_verbose(iface: &str, net: &NetworkDef, wait: u32, dns: &str) -> Result<(), String> {
|
||||
let wait_s = wait.to_string();
|
||||
|
||||
if let Some(profile) = first_profile_for_ssid(&net.ssid) {
|
||||
// Update the saved PSK and, for hidden networks, ensure the flag is set.
|
||||
if !net.password.is_empty() {
|
||||
let _ = run(
|
||||
"nmcli",
|
||||
&[
|
||||
"connection",
|
||||
"modify",
|
||||
&profile,
|
||||
"802-11-wireless-security.psk",
|
||||
net.password.as_str(),
|
||||
],
|
||||
Duration::from_secs(6),
|
||||
);
|
||||
}
|
||||
if net.hidden {
|
||||
let _ = run(
|
||||
"nmcli",
|
||||
&[
|
||||
"connection",
|
||||
"modify",
|
||||
&profile,
|
||||
"802-11-wireless.hidden",
|
||||
"yes",
|
||||
],
|
||||
Duration::from_secs(6),
|
||||
);
|
||||
}
|
||||
let o = run(
|
||||
"nmcli",
|
||||
&["--wait", &wait_s, "connection", "up", &profile, "ifname", iface],
|
||||
Duration::from_secs(wait as u64 + 15),
|
||||
);
|
||||
if !o.success {
|
||||
let detail = o.stderr.trim().to_string();
|
||||
return Err(if detail.is_empty() {
|
||||
o.stdout.trim().to_string()
|
||||
} else {
|
||||
detail
|
||||
});
|
||||
}
|
||||
if let Some(uuid) = active_uuid(iface) {
|
||||
enforce_dns(&uuid, iface, dns);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// No saved profile — create one via device wifi connect.
|
||||
let hidden = if net.hidden { "yes" } else { "no" };
|
||||
let args = [
|
||||
"--wait",
|
||||
|
|
|
|||
17
src/watch.rs
17
src/watch.rs
|
|
@ -67,10 +67,7 @@ fn spawn_nm_monitor(tx: mpsc::Sender<()>) {
|
|||
let l = line.to_lowercase();
|
||||
let interesting = l.contains("disconnect")
|
||||
|| l.contains("unavailable")
|
||||
|| l.contains("connected")
|
||||
|| l.contains("connection")
|
||||
|| l.contains("now")
|
||||
|| l.contains("state");
|
||||
|| l.contains("failed");
|
||||
if interesting && last.elapsed() > Duration::from_millis(1500) {
|
||||
last = Instant::now();
|
||||
let _ = tx.send(());
|
||||
|
|
@ -126,6 +123,8 @@ pub fn run(mut cfg: Config, run_initial: bool) -> i32 {
|
|||
let mut prev_health: Option<Health> = None;
|
||||
let mut prev_profile = profile.clone();
|
||||
let mut fail_streak: u32 = 0;
|
||||
let mut last_flow_at: Option<Instant> = None;
|
||||
const FLOW_COOLDOWN: u64 = 20;
|
||||
|
||||
loop {
|
||||
// Reload config + state so edits and `profile set` take effect live.
|
||||
|
|
@ -146,6 +145,7 @@ pub fn run(mut cfg: Config, run_initial: bool) -> i32 {
|
|||
);
|
||||
prev_profile = profile.clone();
|
||||
prev_health = None; // force re-evaluation/recovery for new profile
|
||||
last_flow_at = None; // allow immediate recovery on profile change
|
||||
}
|
||||
|
||||
let (health, ssid) = classify(&cfg, &profile);
|
||||
|
|
@ -199,17 +199,26 @@ pub fn run(mut cfg: Config, run_initial: bool) -> i32 {
|
|||
Urgency::Normal,
|
||||
);
|
||||
}
|
||||
let elapsed = last_flow_at.map(|t| t.elapsed().as_secs()).unwrap_or(u64::MAX);
|
||||
if elapsed >= FLOW_COOLDOWN {
|
||||
log(&format!(
|
||||
"watch: down ({:?}) profile={profile} ssid={:?} — running flow",
|
||||
health, ssid
|
||||
));
|
||||
let outcome = flow::run(&cfg, &profile);
|
||||
log(&format!("watch: recovery outcome = {:?}", outcome));
|
||||
last_flow_at = Some(Instant::now());
|
||||
fail_streak = if outcome.ok() {
|
||||
0
|
||||
} else {
|
||||
fail_streak.saturating_add(1)
|
||||
};
|
||||
} else {
|
||||
log(&format!(
|
||||
"watch: down ({:?}) — cooldown ({elapsed}s/{FLOW_COOLDOWN}s), skipping flow",
|
||||
health
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue