-
Notifications
You must be signed in to change notification settings - Fork 21
Windows + cx_Freeze GUI build: create_subprocess_exec() fails with OSError [Errno 9] bad file descriptor unless stdin=DEVNULL #126
Description
Summary
On Windows, winloop subprocess creation appears to fail inside a cx_Freeze GUI-frozen executable (base="gui" / no console), even for a minimal repro.
The failure happens when calling asyncio.create_subprocess_exec(...) with the usual defaults for stdin and with redirected stdout/stderr.
A reliable workaround on the application side is to explicitly pass:
stdin=asyncio.subprocess.DEVNULLOnce I do that, the frozen executable works.
This suggests winloop may be assuming that inherited/default stdio handles are valid in a no-console frozen process, while cx_Freeze GUI executables do not always provide valid default stdin/stdio state.
Environment
- OS: Windows x64
- Python:
3.11.15(MSC v.1944 64 bit (AMD64)) winloop:0.5.0cx_Freeze:8.6.1
Real-world trigger
This first showed up in my application when I was doing:
process = await asyncio.create_subprocess_exec(
ffmpegPath,
"-version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)Inside a cx_Freeze GUI-frozen executable, that crashes with:
OSError: [Errno 9] bad file descriptor
Minimal reproduction
smoke.py
import asyncio
import os
import winloop
async def main():
cmd = os.environ.get("ComSpec") or r"C:\Windows\System32\cmd.exe"
proc = await asyncio.create_subprocess_exec(
cmd,
"/c",
"echo",
"123",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)
stdout, _ = await proc.communicate()
print(proc.returncode, stdout)
if __name__ == "__main__":
winloop.install()
asyncio.run(main())setup_cxfreeze.py
from cx_Freeze import Executable, setup
setup(
name="winloop-cxfreeze-smoke",
version="0",
options={
"build_exe": {
"packages": ["winloop"],
}
},
executables=[
Executable(
script="smoke.py",
base="gui",
target_name="winloop-cxfreeze-smoke.exe",
)
],
)Build and run
python setup_cxfreeze.py build_exe
.\build\exe.win-amd64-3.11\winloop-cxfreeze-smoke.exeActual result
The frozen executable crashes with:
Traceback (most recent call last):
File "_tmp_cxfreeze_minimal.py", line 34, in <module>
File "_tmp_cxfreeze_minimal.py", line 29, in run
File "C:\Develop Tools\Python311\Lib\asyncio\runners.py", line 190, in run
return runner.run(main)
File "C:\Develop Tools\Python311\Lib\asyncio\runners.py", line 118, in run
return self._loop.run_until_complete(task)
File "winloop/loop.pyx", line 1615, in winloop.loop.Loop.run_until_complete
File "_tmp_cxfreeze_minimal.py", line 14, in main
File "C:\Develop Tools\Python311\Lib\asyncio\subprocess.py", line 223, in create_subprocess_exec
transport, protocol = await loop.subprocess_exec(
File "winloop/loop.pyx", line 2979, in subprocess_exec
File "winloop/loop.pyx", line 2910, in __subprocess_run
File "winloop/handles/process.pyx", line 673, in winloop.loop.UVProcessTransport.new
File "winloop/handles/process.pyx", line 151, in winloop.loop.UVProcess._init
OSError: [Errno 9] bad file descriptor
The important part is that the failure is happening inside winloop subprocess setup / spawn on Windows in the frozen GUI process.
Expected result
The frozen GUI executable should be able to start subprocesses successfully, even when the parent process does not have a normal console/stdin handle.
At minimum, I would expect behavior similar to “invalid default stdio is ignored or replaced with NUL/DEVNULL” rather than surfacing EBADF from process creation.
Workaround
If I change the subprocess call to explicitly set stdin to DEVNULL, the same frozen executable works:
proc = await asyncio.create_subprocess_exec(
cmd,
"/c",
"echo",
"123",
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL,
)This succeeds in the frozen cx_Freeze GUI executable.
I applied the same workaround in my real app for ffmpeg, ffprobe, and N_m3u8DL-RE, and that avoided the crash.
Additional investigation
I also reproduced a related Windows issue outside of cx_Freeze by manually closing parent fd 0 before calling create_subprocess_exec(). That made subprocess setup fail with EBADF too, which makes me suspect the same family of problem:
- invalid/default stdio inheritance on Windows
- especially in no-console / GUI / frozen parent processes
So this may be in the winloop Windows subprocess stdio handling path rather than in cx_Freeze itself.
The traceback points at UVProcess._init() where uv_spawn() errors are converted, which makes me think one of the inherited/default stdio descriptors/handles is invalid in the frozen GUI process.