Skip to content

Windows + cx_Freeze GUI build: create_subprocess_exec() fails with OSError [Errno 9] bad file descriptor unless stdin=DEVNULL #126

Description

@XiaoYouChR

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.DEVNULL

Once 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.0
  • cx_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.exe

Actual 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Programmers WantedAdditional Programmers wanted for writing a fix or helping with the review process.bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions