diff --git a/module/service.sh b/module/service.sh index 5def5641..b80c2102 100755 --- a/module/service.sh +++ b/module/service.sh @@ -42,6 +42,94 @@ start_daemon() { # Log: /data/local/tmp/awl_pulse.log (root-owned fd, inherited by the child). # Every skip path writes its reason there — a silent skip is what made "no # sound" undiagnosable after a reflash. +PULSE_PROBE_TIMEOUT=3 +PULSE_PROBE_POLLS=6 +PULSE_START_RETRIES=3 + +stop_pulse() { + # The runtime copy is disposable, but the process must be stopped before it + # is replaced. Otherwise an old instance can keep the old UID/audio state + # alive across a module update or an APK reinstall. + if pkill -f "$PAR/bin/pulseaudio" 2>/dev/null; then + sleep 1 + # A stuck instance must not survive into the next attempt and race the + # new server for the socket or AudioFlinger track. + pkill -KILL -f "$PAR/bin/pulseaudio" 2>/dev/null || true + fi + rm -f "$RT/pulse.sock" +} + +pulse_query() { + # pactl in the staged tree is dynamically linked against the staged libpulse; + # use the same runtime environment as the server. timeout is provided by + # Android toybox and prevents a half-created socket from blocking boot. + PULSE_SERVER="unix:$RT/pulse.sock" \ + HOME="$PH" TMPDIR="$PH" \ + LD_LIBRARY_PATH="$PAR/lib:$PAR/lib/pulseaudio:$PAR/lib/pulseaudio/modules" \ + timeout "$PULSE_PROBE_TIMEOUT" "$PAR/bin/pactl" "$@" +} + +pulse_is_ready() { + [ -S "$RT/pulse.sock" ] || return 1 + [ -x "$PAR/bin/pactl" ] || { + echo "anland: pulse probe unavailable ($PAR/bin/pactl missing)" >> "$LOG" + return 1 + } + + SINKS=$(pulse_query list short sinks 2>>"$LOG") + status=$? + if [ "$status" -ne 0 ]; then + echo "anland: pulse probe failed (pactl status $status)" >> "$LOG" + return 1 + fi + + { + echo "anland: pulse sinks:" + printf '%s\n' "$SINKS" + } >> "$LOG" + + # A native socket and a successful pactl connection are insufficient: the + # daemon can otherwise fall back to module-always-sink/auto_null after + # OpenSL ES rejects the stream. SUSPENDED is valid here when idle. + printf '%s\n' "$SINKS" | awk \ + '$2 == "android" && $3 == "module-sles-sink.c" { found = 1 } + END { exit(found ? 0 : 1) }' +} + +wait_for_pulse() { + probe=1 + while [ "$probe" -le "$PULSE_PROBE_POLLS" ]; do + if pulse_is_ready; then + return 0 + fi + if [ "$probe" -lt "$PULSE_PROBE_POLLS" ]; then + sleep 1 + fi + probe=$((probe + 1)) + done + return 1 +} + +launch_pulse() { + attempt="$1" + rm -rf "$PH" + mkdir -p "$PH/run" "$PH/state" || { + echo "anland: pulse attempt $attempt failed (cannot create $PH)" >> "$LOG" + return 1 + } + chown -R "$PAUID:$PAUID" "$PH" + chmod 700 "$PH" "$PH/run" "$PH/state" + rm -f "$RT/pulse.sock" + echo "anland: pulse starting as uid $PAUID (attempt $attempt/$PULSE_START_RETRIES; tree $PAR, socket $RT/pulse.sock)" >> "$LOG" + nohup su "$PAUID" -c "export HOME='$PH' TMPDIR='$PH' PULSE_RUNTIME_PATH='$PH/run' \ +PULSE_STATE_PATH='$PH/state' PULSE_CONFIG_PATH='$PAR/etc/pulse' \ +LD_LIBRARY_PATH='$PAR/lib:$PAR/lib/pulseaudio:$PAR/lib/pulseaudio/modules'; \ +exec '$PAR/bin/pulseaudio' --daemonize=no --exit-idle-time=-1 --disallow-exit \ +--log-target=stderr -n -F '$PAR/etc/pulse/default.pa' \ +-L 'module-native-protocol-unix auth-anonymous=1 socket=$RT/pulse.sock'" \ + >> "$LOG" 2>&1 & +} + start_pulse() { PA="$MODDIR/pulse" LOG=/data/local/tmp/awl_pulse.log @@ -58,25 +146,43 @@ start_pulse() { PAR="$RT/pulse" # a previous instance (repair path, or an app reinstall that changed the # uid) still holds the old tree and socket — replace it whole - pkill -f "$PAR/bin/pulseaudio" 2>/dev/null && sleep 1 + PH="$RT/pulse-home" + stop_pulse rm -rf "$PAR" - cp -r "$PA" "$PAR" + cp -r "$PA" "$PAR" || { + echo "anland: pulse failed (cannot copy $PA to $PAR)" > "$LOG" + return 1 + } chmod -R 755 "$PAR" chcon u:object_r:awl_daemon_exec:s0 "$PAR/bin/pulseaudio" 2>/dev/null # module lookup: daemon.conf is read from PULSE_CONFIG_PATH (this copy) echo "dl-search-path = $PAR/lib/pulseaudio/modules" >> "$PAR/etc/pulse/daemon.conf" - PH="$RT/pulse-home" - rm -rf "$PH"; mkdir -p "$PH/run" "$PH/state" - chown -R "$PAUID:$PAUID" "$PH"; chmod 700 "$PH" "$PH/run" "$PH/state" + + attempt=1 + while [ "$attempt" -le "$PULSE_START_RETRIES" ]; do + stop_pulse + launch_pulse "$attempt" + if wait_for_pulse; then + echo "anland: pulse ready (android/module-sles-sink.c)" >> "$LOG" + return 0 + fi + + echo "anland: pulse attempt $attempt/$PULSE_START_RETRIES did not produce an Android sink" >> "$LOG" + stop_pulse + if [ "$attempt" -lt "$PULSE_START_RETRIES" ]; then + case "$attempt" in + 1) delay=1 ;; + 2) delay=2 ;; + *) delay=4 ;; + esac + sleep "$delay" + fi + attempt=$((attempt + 1)) + done + rm -f "$RT/pulse.sock" - echo "anland: pulse starting as uid $PAUID (tree $PAR, socket $RT/pulse.sock)" > "$LOG" - nohup su "$PAUID" -c "export HOME='$PH' TMPDIR='$PH' PULSE_RUNTIME_PATH='$PH/run' \ -PULSE_STATE_PATH='$PH/state' PULSE_CONFIG_PATH='$PAR/etc/pulse' \ -LD_LIBRARY_PATH='$PAR/lib:$PAR/lib/pulseaudio:$PAR/lib/pulseaudio/modules'; \ -exec '$PAR/bin/pulseaudio' --daemonize=no --exit-idle-time=-1 --disallow-exit \ ---log-target=stderr -n -F '$PAR/etc/pulse/default.pa' \ --L 'module-native-protocol-unix auth-anonymous=1 socket=$RT/pulse.sock'" \ - >> "$LOG" 2>&1 & + echo "anland: pulse failed after $PULSE_START_RETRIES attempts (no android/module-sles-sink.c)" >> "$LOG" + return 1 } case "${1:-}" in