Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,9 @@ If `XDG_CONFIG_HOME` is unset or not an absolute path, they are saved under
`~/.config/bt-intercom/headsets`. Later, run `bt-intercom run --connect` to
restore them. `--connect` retries disconnected headsets every 30 seconds; omit
it if another Bluetooth manager keeps them connected. Routing is polled every
two seconds by default; change that with `--interval SECONDS`. Ctrl-C or SIGTERM
closes links created by this process. Existing PipeWire links are not modified.
two seconds by default; change that with `--interval SECONDS` (minimum: 1 second).
Ctrl-C or SIGTERM closes links created by this process. Existing PipeWire links
are not modified.
Shutdown also cancels and joins background workers, including idle headset
button readers. `--sco-limit N` sets the maximum SCO/eSCO capacity budget per
controller (default: 1). In `sco-a2dp` transport, it limits simultaneous talkers
Expand Down
2 changes: 1 addition & 1 deletion man/bt-intercom.1
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ the selected group, and Space to toggle headset membership.
Set the scan duration from 1 through 300 seconds (default: 15).
.TP
.BI run " --interval SECONDS"
Set the routing update interval (default: 2 seconds).
Set the routing update interval (default: 2 seconds; minimum: 1 second).
.TP
.BI run " --sco-limit N"
Set the maximum simultaneous SCO/eSCO links per Bluetooth controller (default:
Expand Down
2 changes: 1 addition & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub(crate) enum CliCommand {
Run {
#[arg(value_parser = parse_address)]
addresses: Vec<Address>,
/// Routing update interval in seconds.
/// Routing update interval in seconds (minimum: 1).
#[arg(long, default_value_t = 2.0, value_parser = parse_interval)]
interval: f64,
/// Reconnect disconnected headsets every 30 seconds.
Expand Down
23 changes: 23 additions & 0 deletions src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ pub(crate) fn command_cancellable(
.inspect_err(|error| {
log::debug!("{name} stopped after {:?}: {error}", started.elapsed())
})?;
while !output.is_finished() || !error.is_finished() {
if started.elapsed() >= timeout {
kill_process_group(&mut child);
let _ = output.join();
let _ = error.join();
return Err(format!("{name} timed out while draining command output"));
}
thread::sleep(CHILD_POLL_INTERVAL);
}
let stdout = output
.join()
.map_err(|_| "stdout reader panicked".to_string())?
Expand Down Expand Up @@ -254,4 +263,18 @@ mod tests {
);
assert!(start.elapsed() < Duration::from_secs(1));
}

#[test]
fn exited_child_cannot_leave_descendants_holding_output_pipes() {
let start = Instant::now();
assert!(
command(
&["sh", "-c", "sleep 30 & exit 0"],
Duration::from_millis(100)
)
.unwrap_err()
.contains("timed out while draining command output")
);
assert!(start.elapsed() < Duration::from_secs(1));
}
}
9 changes: 5 additions & 4 deletions src/runtime_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ pub(crate) fn parse_interval(value: &str) -> Result<f64, String> {
if !interval.is_finite() || interval <= 0.0 || interval >= u64::MAX as f64 {
return Err("--interval must be a positive finite number".into());
}
if Duration::from_secs_f64(interval).is_zero() {
return Err("--interval must be at least one nanosecond".into());
if interval < 1.0 {
return Err("--interval must be at least 1 second".into());
}
Ok(interval)
}
Expand Down Expand Up @@ -369,7 +369,8 @@ mod tests {
}

#[test]
fn rejects_intervals_that_round_to_zero() {
assert!(parse_interval("0.00000000001").is_err());
fn routing_interval_must_be_at_least_one_second() {
assert!(parse_interval("0.999").is_err());
assert_eq!(parse_interval("1").unwrap(), 1.0);
}
}