- Added `Cargo.toml` to define dependencies and project metadata. - Implemented core CLI functionality for managing virtual microphones (`create`, `edit`, `delete`, `list`, `route`, and `wipe` commands) using `clap`. - Integrated `rusqlite` for persistent state storage and `pipewire` to manage PipeWire nodes. - Ensured graceful handling of feedback loops and system defaults during virtual mic creation and deletion. - Added error handling via `thiserror` for cleaner error definitions.
104 lines
3.7 KiB
Rust
104 lines
3.7 KiB
Rust
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<u32> {
|
|
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::<libc::pid_t>() 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<String> {
|
|
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<String> {
|
|
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)
|
|
}
|