fix(media): update fallback process exit status after wait (#2473)

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
This commit is contained in:
Rudy Celekli
2026-09-30 23:05:32 -04:00
parent 0834c8be28
commit 50775cdc6a
4 changed files with 51 additions and 1 deletions
+2
View File
@@ -86,6 +86,8 @@ metadata and the backend fallback mirror it.
- The Twilio guide and integration directory describe the guided setup and in-app integration pages (#2304)
### Fixed
- Media subprocess fallback reports the completed exit status after waiting (#2473) — thanks @rudycelekli!
- Dub assembly, cached segments and audio tools can read generated WAV files when TorchCodec is missing or cannot load (#2379) — thanks @tokutei58301-boop!
- Remote API-key and share-PIN clients can record and read export history while native filesystem operations stay local (#2383, #2384) — thanks @sedatdagg!
- Concurrent job events receive unique sequence numbers (#2384) — thanks @sedatdagg!
+3 -1
View File
@@ -432,7 +432,9 @@ async def _spawn_thread_fallback(cmd, **kwargs):
return out, err
async def wait(self):
return await loop.run_in_executor(None, self._popen.wait)
code = await loop.run_in_executor(None, self._popen.wait)
self.returncode = code
return code
def kill(self):
self._popen.kill()
+15
View File
@@ -0,0 +1,15 @@
# Update fallback process exit status after wait
The Windows-compatible subprocess wrapper exposes the completed child exit code after `wait()`, matching its public asyncio process contract.
## Contract
Store the completed Popen wait result in the wrapper returncode before returning it.
The unsupported transport condition is reproduced with a selector loop on macOS; native Windows execution and the full application smoke matrix await hosted CI. No ffmpeg binary or model was needed for the process protocol regression.
## Regression coverage
The public helper regression is in `tests/test_voice_fallback_exit.py`.
Run it with `python -m pytest -q tests/test_voice_fallback_exit.py` from the repository root.
It uses local text, files or child processes; no model generation is required.
+31
View File
@@ -0,0 +1,31 @@
"""Real children on the unsupported-subprocess loop retain asyncio exit status."""
import asyncio
import sys
import pytest
class UnsupportedSubprocessLoop(asyncio.SelectorEventLoop):
async def _make_subprocess_transport(self, *args, **kwargs):
raise NotImplementedError
@pytest.mark.parametrize("code", [0, 23])
def test_public_spawn_wait_updates_returncode_on_fallback(code):
from services.ffmpeg_utils import spawn_subprocess
async def run():
proc = await spawn_subprocess(sys.executable, "-c", f"raise SystemExit({code})")
assert proc.uses_sync_pipes
assert await asyncio.wait_for(proc.wait(), 5) == code
assert proc.returncode == code
assert await proc.wait() == code
with asyncio.Runner(loop_factory=UnsupportedSubprocessLoop) as runner:
runner.run(run())
def test_fallback_communicate_keeps_exit_code_and_real_output():
from services.ffmpeg_utils import spawn_subprocess
async def run():
proc = await spawn_subprocess(sys.executable, "-c", "import sys; sys.stdout.buffer.write(b'media child\\n'); raise SystemExit(7)", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
out, err = await asyncio.wait_for(proc.communicate(), 5)
assert out == b"media child\n"
assert err == b""
assert proc.returncode == 7
with asyncio.Runner(loop_factory=UnsupportedSubprocessLoop) as runner:
runner.run(run())