use crate::error::Result; use crate::pw::{loopback, text, PwGraph}; use crate::state; use crate::ui; use std::process::Command; pub fn run() -> Result<()> { let graph = PwGraph::connect()?; let mods_removed = wipe_loopback_modules(&graph)?; let links_removed = graph.unlink_all_into_prefix("vmic_")?; let procs_killed = wipe_pw_loopback_processes(); let conn = state::open_db()?; let rows_removed = conn.execute("DELETE FROM vmics", [])? as u32; restore_defaults(); if mods_removed + links_removed + procs_killed + rows_removed == 0 { ui::info("Nothing to wipe: no virtual mics or vmic loopbacks found."); } else { ui::ok("Wiped all virtual mics:"); println!(" loopback modules unloaded: {mods_removed}"); println!(" links removed: {links_removed}"); println!(" pw-loopback processes killed: {procs_killed}"); println!(" state rows removed: {rows_removed}"); } Ok(()) } /// Unloads every loopback module referencing a vmic_* node, tracked or not. fn wipe_loopback_modules(_graph: &PwGraph) -> Result { let ids = text::list_vmic_loopback_module_ids()?; let mut removed = 0; for id in ids { if loopback::unload_pulse_module(id).is_ok() { removed += 1; } } Ok(removed) } /// Kills every pw-loopback process whose cmdline mentions a vmic_* node, /// tracked or not. SIGTERM only - no SIGKILL escalation, unlike /// `terminate_stage`. fn wipe_pw_loopback_processes() -> u32 { let mut killed = 0; let Ok(entries) = std::fs::read_dir("/proc") else { return 0; }; for entry in entries.flatten() { let Ok(pid) = entry.file_name().to_string_lossy().parse::() else { continue; }; let comm = std::fs::read_to_string(format!("/proc/{pid}/comm")).unwrap_or_default(); if comm.trim() != "pw-loopback" { continue; } let Ok(cmdline) = std::fs::read(format!("/proc/{pid}/cmdline")) else { continue; }; if String::from_utf8_lossy(&cmdline).contains("vmic_") && unsafe { libc::kill(pid, libc::SIGTERM) } == 0 { killed += 1; } } killed } /// If the default sink/source pointed at a vmic node that just got wiped, /// point it back at the first non-vmic device instead of leaving it /// dangling. fn restore_defaults() { if default_output("get-default-sink").is_some_and(|s| s.starts_with("vmic_")) { if let Some(new_sink) = first_non_vmic("sinks", false) { let _ = Command::new("pactl").args(["set-default-sink", &new_sink]).status(); ui::info(&format!("Default sink restored to '{new_sink}'.")); } } if default_output("get-default-source").is_some_and(|s| s.starts_with("vmic_")) { if let Some(new_source) = first_non_vmic("sources", true) { let _ = Command::new("pactl").args(["set-default-source", &new_source]).status(); ui::info(&format!("Default source restored to '{new_source}'.")); } } } fn default_output(subcommand: &str) -> Option { let out = Command::new("pactl").arg(subcommand).output().ok()?; let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); (!s.is_empty()).then_some(s) } fn first_non_vmic(kind: &str, exclude_monitors: bool) -> Option { let out = Command::new("pactl").args(["list", "short", kind]).output().ok()?; String::from_utf8_lossy(&out.stdout) .lines() .filter_map(|l| l.split_whitespace().nth(1)) .find(|name| !name.starts_with("vmic_") && !(exclude_monitors && name.ends_with(".monitor"))) .map(str::to_string) }