Fix update looping and nmcli duplicate profiles
This commit is contained in:
parent
089702a112
commit
25cdbd27f2
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.
|
/// Connect to a network and pin DNS. Returns true only if associated.
|
||||||
pub fn connect(iface: &str, net: &NetworkDef, wait: u32, dns: &str) -> bool {
|
pub fn connect(iface: &str, net: &NetworkDef, wait: u32, dns: &str) -> bool {
|
||||||
connect_verbose(iface, net, wait, dns).is_ok()
|
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.
|
/// Connect to a network and pin DNS. Returns the nmcli error on failure.
|
||||||
///
|
///
|
||||||
/// Uses `nmcli device wifi connect ... password <psk>` so the provided PSK
|
/// Reuses an existing saved profile for the SSID when one exists (updating its
|
||||||
/// always takes effect, even when NM already has a stale saved profile for
|
/// PSK) so that repeated connections do not accumulate numbered duplicates in
|
||||||
/// the same SSID. The PSK is briefly visible in /proc/<pid>/cmdline, which is
|
/// NetworkManager ("NCC", "NCC 1", "NCC 2", …). Falls back to
|
||||||
/// an acceptable trade-off for a personal desktop tool.
|
/// `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> {
|
pub fn connect_verbose(iface: &str, net: &NetworkDef, wait: u32, dns: &str) -> Result<(), String> {
|
||||||
let wait_s = wait.to_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 hidden = if net.hidden { "yes" } else { "no" };
|
||||||
let args = [
|
let args = [
|
||||||
"--wait",
|
"--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 l = line.to_lowercase();
|
||||||
let interesting = l.contains("disconnect")
|
let interesting = l.contains("disconnect")
|
||||||
|| l.contains("unavailable")
|
|| l.contains("unavailable")
|
||||||
|| l.contains("connected")
|
|| l.contains("failed");
|
||||||
|| l.contains("connection")
|
|
||||||
|| l.contains("now")
|
|
||||||
|| l.contains("state");
|
|
||||||
if interesting && last.elapsed() > Duration::from_millis(1500) {
|
if interesting && last.elapsed() > Duration::from_millis(1500) {
|
||||||
last = Instant::now();
|
last = Instant::now();
|
||||||
let _ = tx.send(());
|
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_health: Option<Health> = None;
|
||||||
let mut prev_profile = profile.clone();
|
let mut prev_profile = profile.clone();
|
||||||
let mut fail_streak: u32 = 0;
|
let mut fail_streak: u32 = 0;
|
||||||
|
let mut last_flow_at: Option<Instant> = None;
|
||||||
|
const FLOW_COOLDOWN: u64 = 20;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Reload config + state so edits and `profile set` take effect live.
|
// 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_profile = profile.clone();
|
||||||
prev_health = None; // force re-evaluation/recovery for new profile
|
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);
|
let (health, ssid) = classify(&cfg, &profile);
|
||||||
|
|
@ -199,17 +199,26 @@ pub fn run(mut cfg: Config, run_initial: bool) -> i32 {
|
||||||
Urgency::Normal,
|
Urgency::Normal,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let elapsed = last_flow_at.map(|t| t.elapsed().as_secs()).unwrap_or(u64::MAX);
|
||||||
|
if elapsed >= FLOW_COOLDOWN {
|
||||||
log(&format!(
|
log(&format!(
|
||||||
"watch: down ({:?}) profile={profile} ssid={:?} — running flow",
|
"watch: down ({:?}) profile={profile} ssid={:?} — running flow",
|
||||||
health, ssid
|
health, ssid
|
||||||
));
|
));
|
||||||
let outcome = flow::run(&cfg, &profile);
|
let outcome = flow::run(&cfg, &profile);
|
||||||
log(&format!("watch: recovery outcome = {:?}", outcome));
|
log(&format!("watch: recovery outcome = {:?}", outcome));
|
||||||
|
last_flow_at = Some(Instant::now());
|
||||||
fail_streak = if outcome.ok() {
|
fail_streak = if outcome.ok() {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
fail_streak.saturating_add(1)
|
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