diff --git a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh index 58770e1b..27f37f49 100755 --- a/Runner/suites/Multimedia/Audio/AudioRecord/run.sh +++ b/Runner/suites/Multimedia/Audio/AudioRecord/run.sh @@ -1,6 +1,7 @@ #!/bin/sh # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. # SPDX-License-Identifier: BSD-3-Clause + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # ---- Source init_env & tools ---- @@ -30,7 +31,7 @@ fi . "$TOOLS/audio_common.sh" TESTNAME="AudioRecord" -RES_SUFFIX="" # Optional suffix for unique result files (e.g., "Config1") +RES_SUFFIX="" # Optional suffix for unique result files (e.g., "Config1") # RES_FILE will be set after parsing command-line arguments # Pre-parse --res-suffix for early failure handling @@ -62,7 +63,7 @@ fi # ---------------- Defaults / CLI ---------------- AUDIO_BACKEND="" SRC_CHOICE="${SRC_CHOICE:-mic}" # mic|null -DURATIONS="" # Will be set to default only if using legacy mode +DURATIONS="" # Will be set to default only if using legacy mode RECORD_SECONDS="${RECORD_SECONDS:-30s}" # DEFAULT: 30s; 'auto' maps short/med/long LOOPS="${LOOPS:-1}" TIMEOUT="${TIMEOUT:-0}" # 0 = no watchdog @@ -72,19 +73,19 @@ VERBOSE=0 JUNIT_OUT="" # New config-based testing options -CONFIG_NAMES="" # Explicit config names to test (e.g., "record_config1 record_config2") -CONFIG_FILTER="" # Filter pattern for configs (e.g., "48KHz" or "2ch") -USE_CONFIG_DISCOVERY="${USE_CONFIG_DISCOVERY:-auto}" # auto|true|false +CONFIG_NAMES="" # Explicit config names to test (e.g., "record_config1 record_config2") +CONFIG_FILTER="" # Filter pattern for configs (e.g., "48KHz" or "2ch") +USE_CONFIG_DISCOVERY="${USE_CONFIG_DISCOVERY:-auto}" # auto|true|false usage() { cat < "$RES_FILE" - exit 0 + log_warn "$TESTNAME: backend not available ($AUDIO_BACKEND) - attempting restart+retry once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + + if ! check_audio_daemon "$AUDIO_BACKEND"; then + log_skip "$TESTNAME SKIP - backend not available: $AUDIO_BACKEND" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi fi # Dependencies per backend @@ -293,6 +300,31 @@ else fi fi +# ----- Control-plane sanity (prevents wpctl/pactl hangs during source selection) ----- +if [ "$AUDIO_BACKEND" = "pipewire" ]; then + if ! audio_pw_ctl_ok 2>/dev/null; then + log_warn "$TESTNAME: wpctl not responsive - attempting restart+retry once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + if ! audio_pw_ctl_ok 2>/dev/null; then + log_skip "$TESTNAME SKIP - PipeWire control-plane not responsive after restart" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + fi +elif [ "$AUDIO_BACKEND" = "pulseaudio" ]; then + if ! audio_pa_ctl_ok 2>/dev/null; then + log_warn "$TESTNAME: pactl not responsive - attempting restart+retry once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + if ! audio_pa_ctl_ok 2>/dev/null; then + log_skip "$TESTNAME SKIP - PulseAudio control-plane not responsive after restart" + echo "$TESTNAME SKIP" > "$RES_FILE" + exit 0 + fi + fi +fi + # ----- Route source (set default; recorder uses default source) ----- SRC_ID="" case "$AUDIO_BACKEND:$SRC_CHOICE" in @@ -482,22 +514,6 @@ auto_secs_for() { esac } -# Prefer virtual capture PCMs (PipeWire/Pulse) over raw hw: when a sound server is present -alsa_pick_virtual_pcm() { - command -v arecord >/dev/null 2>&1 || return 1 - - pcs="$(arecord -L 2>/dev/null | sed -n 's/^[[:space:]]*\([[:alnum:]_][[:alnum:]_]*\)[[:space:]]*$/\1/p')" - - for pcm in pipewire pulse default; do - if printf '%s\n' "$pcs" | grep -m1 -x "$pcm" >/dev/null 2>&1; then - printf '%s\n' "$pcm" - return 0 - fi - done - - return 1 -} - # ------------- Test Execution (Matrix or Config Discovery) ------------- total=0 pass=0 @@ -508,7 +524,7 @@ suite_rc=0 if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then # ========== NEW: Config Discovery Mode ========== log_info "Using config discovery mode" - + # Discover and filter configs if [ -n "$CONFIG_NAMES" ] || [ -n "$CONFIG_FILTER" ]; then # Use discover_and_filter_record_configs helper (logs go to stderr automatically) @@ -527,21 +543,21 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then exit 0 } fi - + if [ -z "$CONFIGS_TO_TEST" ]; then log_skip "$TESTNAME SKIP - No valid record configs found" echo "$TESTNAME SKIP" > "$RES_FILE" exit 0 fi - + # Count configs config_count=0 for config in $CONFIGS_TO_TEST; do config_count=$(expr $config_count + 1) done - + log_info "Discovered $config_count configs to test" - + # Test each config for config in $CONFIGS_TO_TEST; do # Generate test case name @@ -549,40 +565,40 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then log_warn "Skipping config with invalid name: $config" continue } - + # Get recording parameters params="$(get_record_config_params "$config")" || { log_warn "Skipping config with invalid parameters: $config" continue } - + rate="$(printf '%s' "$params" | awk '{print $1}')" channels="$(printf '%s' "$params" | awk '{print $2}')" - + total=$(expr $total + 1) logf="$LOGDIR/${case_name}.log" : > "$logf" export AUDIO_LOGCTX="$logf" - + log_info "[$case_name] Using config: $config (rate=${rate}Hz channels=$channels)" - + # Determine recording duration secs="$RECORD_SECONDS" if [ "$secs" = "auto" ]; then - secs="5s" # Default for config discovery mode + secs="5s" # Default for config discovery mode fi - + i=1 ok_runs=0 last_elapsed=0 - + while [ "$i" -le "$LOOPS" ]; do iso="$(date -u +%Y-%m-%dT%H:%M:%SZ)" effective_timeout="$secs" if [ -n "$TIMEOUT" ] && [ "$TIMEOUT" != "0" ]; then effective_timeout="$TIMEOUT" fi - + loop_hdr="source=$SRC_CHOICE" if [ "$AUDIO_BACKEND" = "pipewire" ]; then if [ -n "$SRC_ID" ]; then @@ -593,20 +609,33 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then else loop_hdr="$loop_hdr($SRC_LABEL)" fi - + log_info "[$case_name] loop $i/$LOOPS start=$iso rate=${rate}Hz channels=$channels backend=$AUDIO_BACKEND $loop_hdr" - + out="$LOGDIR/${case_name}.wav" : > "$out" - + start_s="$(date +%s 2>/dev/null || echo 0)" - + if [ "$AUDIO_BACKEND" = "pipewire" ]; then log_info "[$case_name] exec: pw-record -v --rate=$rate --channels=$channels \"$out\"" audio_exec_with_timeout "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - + + # If pw-record failed AND PipeWire control-plane is broken, restart+retry once + if [ "$rc" -ne 0 ] && ! audio_pw_ctl_ok 2>/dev/null; then + log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - restarting and retrying once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + out="$LOGDIR/${case_name}.wav" + : > "$out" + log_info "[$case_name] retry after restart: pw-record -v --rate=$rate --channels=$channels \"$out\"" + audio_exec_with_timeout "$effective_timeout" pw-record -v --rate="$rate" --channels="$channels" "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + fi + # If we already got real audio, accept and skip fallbacks if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then if [ "$rc" -ne 0 ]; then @@ -627,7 +656,7 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" fi fi - + # As a last resort, retry pw-record with --target (only if we have a source id) if { [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; } && [ -n "$SRC_ID" ]; then : > "$out" @@ -637,7 +666,7 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" fi fi - + # (Optional safety) If nonzero rc but output is clearly valid, accept. if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then log_warn "[$case_name] nonzero rc==$rc but recording looks valid (bytes=$bytes) - PASS" @@ -652,14 +681,14 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then arecord -D "$SRC_ID" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - + if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then if printf '%s\n' "$SRC_ID" | grep -q '^hw:'; then alt_dev="plughw:${SRC_ID#hw:}" else alt_dev="$SRC_ID" fi - + # Try with the specific config parameters : > "$out" log_info "[$case_name] retry: arecord -D \"$alt_dev\" -f S16_LE -r $rate -c $channels -d $secs_int \"$out\"" @@ -667,7 +696,7 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then arecord -D "$alt_dev" -f S16_LE -r "$rate" -c "$channels" -d "$secs_int" "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" - + # If still failing, try fallback combinations if [ "$rc" -ne 0 ] || [ "${bytes:-0}" -le 1024 ] 2>/dev/null; then for combo in "S16_LE 48000 2" "S16_LE 44100 2" "S16_LE 16000 1"; do @@ -687,7 +716,7 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then done fi fi - + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" rc=0 @@ -698,34 +727,48 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then audio_exec_with_timeout "$effective_timeout" parecord --rate="$rate" --channels="$channels" --file-format=wav "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + + # If parecord failed AND PulseAudio control-plane is broken, restart+retry once + if [ "$rc" -ne 0 ] && ! audio_pa_ctl_ok 2>/dev/null; then + log_warn "[$case_name] parecord rc=$rc and pactl not responsive - restarting and retrying once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + out="$LOGDIR/${case_name}.wav" + : > "$out" + log_info "[$case_name] retry after restart: parecord --rate=$rate --channels=$channels --file-format=wav \"$out\"" + audio_exec_with_timeout "$effective_timeout" parecord --rate="$rate" --channels="$channels" --file-format=wav "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + fi ++ if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" rc=0 fi fi fi - + end_s="$(date +%s 2>/dev/null || echo 0)" last_elapsed=$(expr $end_s - $start_s) [ "$last_elapsed" -lt 0 ] && last_elapsed=0 - + # Evidence pw_ev=$(audio_evidence_pw_streaming || echo 0) pa_ev=$(audio_evidence_pa_streaming || echo 0) - + if [ "$AUDIO_BACKEND" = "pulseaudio" ] && [ "$pa_ev" -eq 0 ]; then if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then pa_ev=1 fi fi - + alsa_ev=$(audio_evidence_alsa_running_any || echo 0) asoc_ev=$(audio_evidence_asoc_path_on || echo 0) pwlog_ev=$(audio_evidence_pw_log_seen || echo 0) if [ "$AUDIO_BACKEND" = "pulseaudio" ]; then pwlog_ev=0 fi - + if [ "$alsa_ev" -eq 0 ]; then if [ "$AUDIO_BACKEND" = "pipewire" ] && [ "$pw_ev" -eq 1 ]; then alsa_ev=1 @@ -734,31 +777,31 @@ if [ "$USE_CONFIG_DISCOVERY" = "true" ]; then alsa_ev=1 fi fi - + if [ "$asoc_ev" -eq 0 ] && [ "$alsa_ev" -eq 1 ]; then asoc_ev=1 fi - + log_info "[$case_name] evidence: pw_streaming=$pw_ev pa_streaming=$pa_ev alsa_running=$alsa_ev asoc_path_on=$asoc_ev bytes=${bytes:-0} pw_log=$pwlog_ev" - + if [ "$rc" -eq 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then log_pass "[$case_name] loop $i OK (rc=0, ${last_elapsed}s, bytes=$bytes)" ok_runs=$(expr $ok_runs + 1) else log_fail "[$case_name] loop $i FAILED (rc=$rc, ${last_elapsed}s, bytes=${bytes:-0}) - see $logf" fi - + i=$(expr $i + 1) done - + # Aggregate result for this config status="FAIL" if [ "$ok_runs" -ge 1 ]; then status="PASS" fi - + append_junit "$case_name" "$last_elapsed" "$status" "$logf" - + case "$status" in PASS) pass=$(expr $pass + 1) @@ -831,6 +874,19 @@ else rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + # If pw-record failed AND PipeWire control-plane is broken, restart+retry once + if [ "$rc" -ne 0 ] && ! audio_pw_ctl_ok 2>/dev/null; then + log_warn "[$case_name] pw-record rc=$rc and wpctl not responsive - restarting and retrying once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + out="$LOGDIR/${case_name}.wav" + : > "$out" + log_info "[$case_name] retry after restart: pw-record -v \"$out\"" + audio_exec_with_timeout "$effective_timeout" pw-record -v "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + fi + # If we already got real audio, accept and skip fallbacks if [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then if [ "$rc" -ne 0 ]; then @@ -909,6 +965,20 @@ else audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$out" >> "$logf" 2>&1 rc=$? bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + + # If parecord failed AND PulseAudio control-plane is broken, restart+retry once + if [ "$rc" -ne 0 ] && ! audio_pa_ctl_ok 2>/dev/null; then + log_warn "[$case_name] parecord rc=$rc and pactl not responsive - restarting and retrying once" + audio_restart_services_best_effort >/dev/null 2>&1 || true + audio_wait_audio_ready 20 >/dev/null 2>&1 || true + out="$LOGDIR/${case_name}.wav" + : > "$out" + log_info "[$case_name] retry after restart: parecord --file-format=wav \"$out\"" + audio_exec_with_timeout "$effective_timeout" parecord --file-format=wav "$out" >> "$logf" 2>&1 + rc=$? + bytes="$(file_size_bytes "$out" 2>/dev/null || echo 0)" + fi + if [ "$rc" -ne 0 ] && [ "${bytes:-0}" -gt 1024 ] 2>/dev/null; then log_warn "[$case_name] nonzero rc=$rc but recording looks valid (bytes=$bytes) - PASS" rc=0 diff --git a/Runner/utils/audio_common.sh b/Runner/utils/audio_common.sh index 1b1b7bc3..16068733 100755 --- a/Runner/utils/audio_common.sh +++ b/Runner/utils/audio_common.sh @@ -1,6 +1,7 @@ #!/bin/sh # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# SPDX-License-Identifier: BSD-3-Clause# Common audio helpers for PipeWire / PulseAudio runners. +# SPDX-License-Identifier: BSD-3-Clause +# Common audio helpers for PipeWire / PulseAudio runners. # Requires: functestlib.sh (log_* helpers, extract_tar_from_url, scan_dmesg_errors) # ---------- Backend detection & daemon checks ---------- @@ -19,6 +20,22 @@ detect_audio_backend() { return 1 } +audio_proc_running() { + name="$1" + [ -z "$name" ] && return 1 + + if command -v pgrep >/dev/null 2>&1; then + pgrep -x "$name" >/dev/null 2>&1; return $? + fi + + if command -v pidof >/dev/null 2>&1; then + pidof "$name" >/dev/null 2>&1; return $? + fi + + # shellcheck disable=SC2009 + ps 2>/dev/null | grep -w "$name" | grep -v grep >/dev/null 2>&1 +} + check_audio_daemon() { case "$1" in pipewire) pgrep -x pipewire >/dev/null 2>&1 ;; @@ -134,12 +151,20 @@ dump_mixers() { out="$1" { echo "---- wpctl status ----" - command -v wpctl >/dev/null 2>&1 && wpctl status 2>&1 || echo "(wpctl not found)" + if command -v wpctl >/dev/null 2>&1; then + audio_exec_with_timeout 2s wpctl status 2>&1 || echo "(wpctl status failed/timeout)" + else + echo "(wpctl not found)" + fi + echo "---- pactl list ----" - command -v pactl >/dev/null 2>&1 && pactl list 2>&1 || echo "(pactl not found)" + if command -v pactl >/dev/null 2>&1; then + audio_exec_with_timeout 3s pactl list 2>&1 || echo "(pactl list failed/timeout)" + else + echo "(pactl not found)" + fi } >"$out" 2>/dev/null } - # Returns child exit code (124 when killed by timeout). If tmo<=0, runs the # command directly (no watchdog). @@ -183,6 +208,75 @@ audio_timeout_run() { wait "$pid"; return $? } +audio_restart_services_best_effort() { + uid="$(id -u 2>/dev/null || echo 0)" + rt="${XDG_RUNTIME_DIR:-/run/user/$uid}" + + # Ensure runtime dir exists (some LAVA/minimal images may not have it) + if [ ! -d "$rt" ] && [ -n "$rt" ]; then + mkdir -p "$rt" 2>/dev/null || true + chmod 700 "$rt" 2>/dev/null || true + fi + [ -d "$rt" ] && export XDG_RUNTIME_DIR="$rt" + + # systemd user + system (best effort, bounded time) + if command -v systemctl >/dev/null 2>&1; then + # optional reloads (some images need this after overlay / unit changes) + audio_exec_with_timeout 10s systemctl --user daemon-reload >/dev/null 2>&1 || true + audio_exec_with_timeout 10s systemctl daemon-reload >/dev/null 2>&1 || true + + audio_exec_with_timeout 10s systemctl --user restart pipewire pipewire-pulse wireplumber pulseaudio >/dev/null 2>&1 || true + audio_exec_with_timeout 10s systemctl restart pipewire pipewire-pulse wireplumber pulseaudio >/dev/null 2>&1 || true + fi + + # If control-plane is OK already, stop here (accept PW or PA) + if audio_pw_ctl_ok 2>/dev/null || audio_pa_ctl_ok 2>/dev/null; then + return 0 + fi + + # hard reset (works without systemd/user session) + if command -v pkill >/dev/null 2>&1; then + pkill -x wireplumber >/dev/null 2>&1 || true + pkill -x pipewire-pulse >/dev/null 2>&1 || true + pkill -x pipewire >/dev/null 2>&1 || true + pkill -x pulseaudio >/dev/null 2>&1 || true + elif command -v killall >/dev/null 2>&1; then + killall -q wireplumber pipewire-pulse pipewire pulseaudio 2>/dev/null || true + fi + + sleep 1 + + # stale sockets/locks + if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -d "$XDG_RUNTIME_DIR" ]; then + rm -f "$XDG_RUNTIME_DIR/pipewire-0" \ + "$XDG_RUNTIME_DIR/pipewire-0.lock" \ + "$XDG_RUNTIME_DIR/pulse/native" \ + "$XDG_RUNTIME_DIR/pulse/pid" \ + "$XDG_RUNTIME_DIR/pulse/cookie" \ + 2>/dev/null || true + fi + + # respawn (best effort, ShellCheck-clean) + if command -v pipewire >/dev/null 2>&1; then + pipewire >/dev/null 2>&1 & + fi + + if command -v wireplumber >/dev/null 2>&1; then + wireplumber >/dev/null 2>&1 & + elif command -v pipewire-media-session >/dev/null 2>&1; then + pipewire-media-session >/dev/null 2>&1 & + fi + + if command -v pipewire-pulse >/dev/null 2>&1; then + pipewire-pulse >/dev/null 2>&1 & + fi + + if command -v pulseaudio >/dev/null 2>&1; then + pulseaudio --start >/dev/null 2>&1 || true + fi + + return 0 +} # Function: setup_overlay_audio_environment # Purpose: Configure audio environment for overlay builds (audioreach-based) # Returns: 0 on success, 1 on failure @@ -222,7 +316,7 @@ setup_overlay_audio_environment() { # Restart PipeWire log_info "Restarting pipewire service..." - if ! systemctl restart pipewire 2>/dev/null; then + if ! audio_exec_with_timeout 15s systemctl restart pipewire 2>/dev/null; then log_fail "Failed to restart pipewire service" return 1 fi @@ -237,7 +331,7 @@ setup_overlay_audio_environment() { # Check if pipewire process is running if pgrep -x pipewire >/dev/null 2>&1; then # Verify wpctl can communicate - if command -v wpctl >/dev/null 2>&1 && wpctl status >/dev/null 2>&1; then + if command -v audio_pw_ctl_ok >/dev/null 2>&1 && audio_pw_ctl_ok; then log_pass "PipeWire is ready (took ${elapsed}s)" return 0 fi @@ -256,10 +350,30 @@ setup_overlay_audio_environment() { log_fail "Check 'systemctl status pipewire' and 'journalctl -u pipewire' for details" return 1 } - + +# ---------- PipeWire control helpers (bounded; never hang) ---------- +pwctl_status_safe() { + # Prints wpctl status on stdout; returns nonzero on timeout/failure. + command -v wpctl >/dev/null 2>&1 || return 1 + audio_exec_with_timeout 2s wpctl status 2>/dev/null +} + +pwctl_inspect_safe() { + # Prints wpctl inspect on stdout; returns nonzero on timeout/failure. + id="$1" + [ -n "$id" ] || return 1 + command -v wpctl >/dev/null 2>&1 || return 1 + audio_exec_with_timeout 2s wpctl inspect "$id" 2>/dev/null +} + +audio_pw_ctl_ok() { + pwctl_status_safe >/dev/null 2>&1 +} # ---------- PipeWire: sinks (playback) ---------- pw_default_speakers() { - _block="$(wpctl status 2>/dev/null | sed -n '/Sinks:/,/Sources:/p')" + st="$(pwctl_status_safe 2>/dev/null)" || { printf '%s\n' ""; return 0; } + + _block="$(printf '%s\n' "$st" | sed -n '/Sinks:/,/Sources:/p')" _id="$(printf '%s\n' "$_block" \ | grep -i -E 'speaker|headphone' \ | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ @@ -274,19 +388,24 @@ pw_default_speakers() { } pw_default_null() { - wpctl status 2>/dev/null \ - | sed -n '/Sinks:/,/Sources:/p' \ - | grep -i -E 'null|dummy|loopback|monitor' \ - | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ - | head -n1 + st="$(pwctl_status_safe 2>/dev/null)" || return 0 + printf '%s\n' "$st" \ + | sed -n '/Sinks:/,/Sources:/p' \ + | grep -i -E 'null|dummy|loopback|monitor' \ + | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' \ + | head -n1 } pw_sink_name_safe() { id="$1"; [ -n "$id" ] || { echo ""; return 1; } - name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" - [ -n "$name" ] || name="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" + + ins="$(pwctl_inspect_safe "$id" 2>/dev/null || true)" + name="$(printf '%s\n' "$ins" | grep -m1 'node.description' | cut -d'"' -f2)" + [ -n "$name" ] || name="$(printf '%s\n' "$ins" | grep -m1 'node.name' | cut -d'"' -f2)" + if [ -z "$name" ]; then - name="$(wpctl status 2>/dev/null \ + st="$(pwctl_status_safe 2>/dev/null || true)" + name="$(printf '%s\n' "$st" \ | sed -n '/Sinks:/,/Sources:/p' \ | grep -E "^[^0-9]*${id}[.][[:space:]]" \ | sed 's/^[^0-9]*[0-9][0-9]*[.][[:space:]][[:space:]]*//' \ @@ -297,30 +416,44 @@ pw_sink_name_safe() { } pw_sink_name() { pw_sink_name_safe "$@"; } # back-compat alias -pw_set_default_sink() { [ -n "$1" ] && wpctl set-default "$1" >/dev/null 2>&1; } +pw_set_default_sink() { + [ -n "$1" ] || return 1 + audio_exec_with_timeout 2s wpctl set-default "$1" >/dev/null 2>&1 +} # ---------- PipeWire: sources (record) ---------- pw_default_mic() { - blk="$(wpctl status 2>/dev/null | sed -n '/Sources:/,/^$/p')" + st="$(pwctl_status_safe 2>/dev/null)" || { printf '%s\n' ""; return 0; } + + blk="$(printf '%s\n' "$st" | sed -n '/Sources:/,/^$/p')" id="$(printf '%s\n' "$blk" | grep -i 'mic' | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | head -n1)" [ -n "$id" ] || id="$(printf '%s\n' "$blk" | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | head -n1)" printf '%s\n' "$id" } pw_default_null_source() { - blk="$(wpctl status 2>/dev/null | sed -n '/Sources:/,/^$/p')" + st="$(pwctl_status_safe 2>/dev/null)" || { printf '%s\n' ""; return 0; } + + blk="$(printf '%s\n' "$st" | sed -n '/Sources:/,/^$/p')" id="$(printf '%s\n' "$blk" | grep -i 'null\|dummy' | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | head -n1)" printf '%s\n' "$id" } -pw_set_default_source() { [ -n "$1" ] && wpctl set-default "$1" >/dev/null 2>&1; } +pw_set_default_source() { + [ -n "$1" ] || return 1 + audio_exec_with_timeout 2s wpctl set-default "$1" >/dev/null 2>&1 +} pw_source_label_safe() { id="$1"; [ -n "$id" ] || { echo ""; return 1; } - label="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.description' | cut -d'"' -f2)" - [ -n "$label" ] || label="$(wpctl inspect "$id" 2>/dev/null | grep -m1 'node.name' | cut -d'"' -f2)" + + ins="$(pwctl_inspect_safe "$id" 2>/dev/null || true)" + label="$(printf '%s\n' "$ins" | grep -m1 'node.description' | cut -d'"' -f2)" + [ -n "$label" ] || label="$(printf '%s\n' "$ins" | grep -m1 'node.name' | cut -d'"' -f2)" + if [ -z "$label" ]; then - label="$(wpctl status 2>/dev/null \ + st="$(pwctl_status_safe 2>/dev/null || true)" + label="$(printf '%s\n' "$st" \ | sed -n '/Sources:/,/Filters:/p' \ | grep -E "^[^0-9]*${id}[.][[:space:]]" \ | sed 's/^[^0-9]*[0-9][0-9]*[.][[:space:]][[:space:]]*//' \ @@ -329,7 +462,6 @@ pw_source_label_safe() { fi printf '%s\n' "$label" } - # ---------- PulseAudio: sinks (playback) ---------- pa_default_speakers() { def="$(pactl info 2>/dev/null | sed -n 's/^Default Sink:[[:space:]]*//p' | head -n1)" @@ -407,7 +539,7 @@ audio_evidence_pw_streaming() { # Try wpctl (fast); fall back to log scan if AUDIO_LOGCTX is available if command -v wpctl >/dev/null 2>&1; then # Count Input/Output streams in RUNNING state - wpctl status 2>/dev/null | grep -Eq 'RUNNING' && { echo 1; return; } + pwctl_status_safe 2>/dev/null | grep -Eq 'RUNNING' && { echo 1; return; } fi # Fallback to log if [ -n "${AUDIO_LOGCTX:-}" ] && [ -r "$AUDIO_LOGCTX" ]; then @@ -652,40 +784,108 @@ audio_parse_secs() { # --- Local watchdog that always honors the first argument (e.g. "15" or "15s") --- audio_exec_with_timeout() { dur="$1"; shift + # normalize: allow "15" or "15s" case "$dur" in ""|"0") dur_norm=0 ;; *s) dur_norm="${dur%s}" ;; *) dur_norm="$dur" ;; esac - - # numeric? if not, treat as no-timeout case "$dur_norm" in *[!0-9]*|"") dur_norm=0 ;; esac - if [ "$dur_norm" -gt 0 ] 2>/dev/null && command -v timeout >/dev/null 2>&1; then - timeout "$dur_norm" "$@"; return $? + # no watchdog + if [ "$dur_norm" -le 0 ] 2>/dev/null; then + "$@" + return $? fi - if [ "$dur_norm" -gt 0 ] 2>/dev/null; then - # portable fallback watchdog - "$@" & - pid=$! - ( - sleep "$dur_norm" - kill -TERM "$pid" 2>/dev/null || true + # Run in background and enforce our own bounded timeout (don't rely on external timeout) + "$@" & + pid=$! + + start="$(date +%s 2>/dev/null || echo 0)" + deadline="$(expr "$start" + "$dur_norm" 2>/dev/null || echo 0)" + + # Wait until exit or deadline + while kill -0 "$pid" 2>/dev/null; do + now="$(date +%s 2>/dev/null || echo 0)" + if [ "$now" -ge "$deadline" ] 2>/dev/null; then + break + fi + sleep 1 + done + + # Timed out: try terminate/kill, but never block forever + if kill -0 "$pid" 2>/dev/null; then + kill -TERM "$pid" 2>/dev/null || true + sleep 1 + kill -KILL "$pid" 2>/dev/null || true + + # bounded grace wait (handles normal killable cases) + grace=0 + while kill -0 "$pid" 2>/dev/null && [ "$grace" -lt 3 ]; do sleep 1 - kill -KILL "$pid" 2>/dev/null || true - ) & - w=$! - wait "$pid"; rc=$? - kill -TERM "$w" 2>/dev/null || true - # map "killed by watchdog" to 124 (GNU timeout convention) - [ "$rc" -eq 143 ] && rc=124 + grace="$(expr "$grace" + 1)" + done + + # Still alive -> likely D-state. Do NOT wait forever. + if kill -0 "$pid" 2>/dev/null; then + return 124 + fi + + wait "$pid" 2>/dev/null + rc=$? + [ "$rc" -eq 143 ] 2>/dev/null && rc=124 return "$rc" fi - # no timeout - "$@" + # Exited naturally before timeout + wait "$pid" 2>/dev/null + return $? +} + +audio_wait_audio_ready() { + max_s="${1:-20}" + i=0 + + while [ "$i" -lt "$max_s" ] 2>/dev/null; do + if command -v wpctl >/dev/null 2>&1 && audio_pw_ctl_ok; then + return 0 + fi + if command -v pactl >/dev/null 2>&1 && audio_pa_ctl_ok; then + return 0 + fi + if [ -d /dev/snd ] || [ -e /proc/asound/cards ]; then + return 0 + fi + sleep 1 + i="$(expr "$i" + 1)" + done + + return 1 +} +# --- bounded wpctl helpers (never hang) --- +pwctl_status_safe() { + # Prints wpctl status to stdout on success, returns nonzero on failure/timeout. + out="$(audio_exec_with_timeout 2s wpctl status 2>/dev/null)" + rc=$? + [ "$rc" -eq 0 ] || return 1 + printf '%s\n' "$out" +} + +audio_pw_ctl_ok() { + pwctl_status_safe >/dev/null 2>&1 +} + +audio_pa_ctl_ok() { + command -v pactl >/dev/null 2>&1 || return 1 + audio_exec_with_timeout 2s pactl info >/dev/null 2>&1 +} +# If you have an existing pw_set_default_source(), replace it with this bounded version. +pw_set_default_source() { + id="$1" + [ -n "$id" ] || return 1 + audio_exec_with_timeout 2s wpctl set-default "$id" >/dev/null 2>&1 } # --------------------------------------------------------------------