fix(voice): allow /voice over SSH when a sound server is reachable (#35719)
SSH sessions hard-failed voice mode on the presence of SSH_* env vars
alone, even when a PulseAudio/PipeWire server is running on the host and
audio works (ffplay/aplay/pw-play -> pulseaudio). Probe the default
sound-server sockets (PULSE_SERVER unix path, PULSE_RUNTIME_PATH/native,
$XDG_RUNTIME_DIR/{pulse/native,pipewire-0}) and actually connect() so a
stale socket doesn't count; downgrade the SSH branch to a notice when
audio is reachable. Mirrors the existing Docker/WSL forwarding handling.
Fixes #35622
This commit is contained in:
@ -85,6 +85,59 @@ def _termux_voice_capture_available() -> bool:
|
||||
return _termux_microphone_command() is not None and _termux_api_app_installed()
|
||||
|
||||
|
||||
def _pulse_socket_reachable() -> bool:
|
||||
"""Return True if a PulseAudio/PipeWire socket is reachable on disk.
|
||||
|
||||
Covers the common case where a sound server runs locally (e.g. on a
|
||||
remote SSH host) without ``PULSE_SERVER``/``PIPEWIRE_REMOTE`` being set --
|
||||
the client just connects to the default socket under the runtime dir.
|
||||
We look at ``PULSE_SERVER`` unix paths, ``PULSE_RUNTIME_PATH``, and
|
||||
``XDG_RUNTIME_DIR`` for a ``pulse/native`` or ``pipewire-0`` socket
|
||||
(issue #35622).
|
||||
"""
|
||||
import socket
|
||||
import stat
|
||||
|
||||
candidates: List[str] = []
|
||||
|
||||
pulse_server = os.environ.get('PULSE_SERVER', '')
|
||||
# PULSE_SERVER may be "unix:/path", "unix:/path;..." or a bare path.
|
||||
for part in pulse_server.split(';'):
|
||||
part = part.strip()
|
||||
if part.startswith('unix:'):
|
||||
candidates.append(part[len('unix:'):])
|
||||
|
||||
pulse_runtime = os.environ.get('PULSE_RUNTIME_PATH')
|
||||
if pulse_runtime:
|
||||
candidates.append(os.path.join(pulse_runtime, 'native'))
|
||||
|
||||
xdg_runtime = os.environ.get('XDG_RUNTIME_DIR')
|
||||
if xdg_runtime:
|
||||
candidates.append(os.path.join(xdg_runtime, 'pulse', 'native'))
|
||||
candidates.append(os.path.join(xdg_runtime, 'pipewire-0'))
|
||||
|
||||
for path in candidates:
|
||||
if not path:
|
||||
continue
|
||||
try:
|
||||
if not stat.S_ISSOCK(os.stat(path).st_mode):
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
# Confirm the socket actually accepts a connection -- a stale socket
|
||||
# file left by a dead server should not count as reachable.
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.settimeout(0.5)
|
||||
sock.connect(path)
|
||||
return True
|
||||
except OSError:
|
||||
continue
|
||||
finally:
|
||||
sock.close()
|
||||
return False
|
||||
|
||||
|
||||
def detect_audio_environment() -> dict:
|
||||
"""Detect if the current environment supports audio I/O.
|
||||
|
||||
@ -98,12 +151,25 @@ def detect_audio_environment() -> dict:
|
||||
termux_app_installed = _termux_api_app_installed()
|
||||
termux_capture = bool(termux_mic_cmd and termux_app_installed)
|
||||
has_forwarded_audio = bool(
|
||||
os.environ.get('PULSE_SERVER') or os.environ.get('PIPEWIRE_REMOTE')
|
||||
os.environ.get('PULSE_SERVER')
|
||||
or os.environ.get('PIPEWIRE_REMOTE')
|
||||
or _pulse_socket_reachable()
|
||||
)
|
||||
|
||||
# SSH detection
|
||||
# SSH detection -- normally no audio devices, but honor a reachable
|
||||
# sound server (PulseAudio/PipeWire socket or forwarding env vars), which
|
||||
# works fine over SSH (issue #35622).
|
||||
if any(os.environ.get(v) for v in ('SSH_CLIENT', 'SSH_TTY', 'SSH_CONNECTION')):
|
||||
warnings.append("Running over SSH -- no audio devices available")
|
||||
if has_forwarded_audio:
|
||||
notices.append("Running over SSH with a reachable PulseAudio/PipeWire sound server")
|
||||
else:
|
||||
warnings.append(
|
||||
"Running over SSH -- no audio devices available.\n"
|
||||
" If a sound server (PulseAudio/PipeWire) is running on this host,\n"
|
||||
" point Hermes at it, e.g.:\n"
|
||||
" export XDG_RUNTIME_DIR=/run/user/$(id -u)\n"
|
||||
" # or: export PULSE_SERVER=unix:$XDG_RUNTIME_DIR/pulse/native"
|
||||
)
|
||||
|
||||
# Docker/Podman container detection — honor host audio forwarding.
|
||||
# When the user mounts a PulseAudio/PipeWire socket into the container
|
||||
|
||||
Reference in New Issue
Block a user