bitcoin wrapper: Fix Windows exec so wrapper waits for child process #36105

pull ryanofsky wants to merge 3 commits into bitcoin:master from ryanofsky:pr/bitwin changing 7 files +29 −21
  1. ryanofsky commented at 5:34 PM on August 27, 2026: contributor

    Problem: On Windows, bitcoin.exe spawns a child process and immediately exits rather than waiting for it to finish. This breaks capturing output and checking exit codes in scripts and tests.

    Solution: Switch util::ExecVp from _execvp to _spawnvp(_P_WAIT), which blocks until the child exits and forwards its exit code. Also enable tool_bitcoin.py and interface_gui.py tests which were skipped due to previous behavior.

    This fix requires small changes to src/util/exec.cpp and src/bitcoin.cpp and there is also an extra commit fixing errno message strings on Windows when ExecVp fails. Details in commit messages.

  2. util: Fix ExecVp on Windows to wait for child process
    Switch from _execvp to _spawnvp(_P_NOWAIT) + _cwait so bitcoin.exe
    waits for the child process to finish and forwards its exit code.
    Previously _execvp would exit the parent as soon as the child started,
    making it impossible for anything waiting on bitcoin.exe (such as a test
    framework) to track whether the child succeeded or failed. _P_NOWAIT is
    used instead of _P_WAIT so that a child exit code of -1 (0xffffffff) is
    not confused with a spawn failure: _spawnvp(_P_WAIT) returns -1 for both,
    but _spawnvp(_P_NOWAIT) returns the child handle on success, and _cwait
    fills a separate status that is forwarded via _exit.
    
    Co-Authored-By: Bortlesboat <169967362+Bortlesboat@users.noreply.github.com>
    This change was written with Claude Sonnet 4.6.
    4453ead327
  3. DrahtBot commented at 5:34 PM on August 27, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/36105.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK hebasto, hodlinator

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #36106 (bitcoin wrapper: respect CMAKE_INSTALL_BINDIR/LIBEXECDIR by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. hebasto commented at 10:17 PM on August 27, 2026: member

    Concept ACK.

  5. hebasto commented at 10:23 PM on August 27, 2026: member

    There is a preliminary understanding among the build crowd that #33593 might land shortly after branching off "32.x", which renders the "bitcoin: Fix msvcrt regressions from _execvp to _spawnvp switch" commit unnecessary.

  6. bitcoin deleted a comment on Aug 31, 2026
  7. in test/functional/test_framework/test_framework.py:1 in 6ca9199eec outdated


    hodlinator commented at 12:06 PM on August 31, 2026:

    In 6ca9199eecd9934563351c6b4b11468b75c6d088 - "test: Enable tool_bitcoin.py and interface_gui.py tests on Windows":

    The commit message refers to _wspawnvp() and _wexecvp(), however we use the non-wide versions in code.



    ryanofsky commented at 1:45 AM on September 9, 2026:

    re: #36105 (review)

    The commit message refers to _wspawnvp() and _wexecvp(), however we use the non-wide versions in code.

    Good catch. These changes are older than #35704 and the commit message was not updated when it was merged.


    ryanofsky commented at 1:46 AM on September 9, 2026:

    re: #36105 (review)

    Thanks. It's important to me to distinguish words and ideas which are my own from those that come from other sources, so I use co-author tags for this. I think the AI policy was written with a different kind of PR in mind than this one. I understand the changes in this PR and take responsibility for them.


    hodlinator commented at 7:25 PM on September 10, 2026:

    thread #36105 (review):

    Thanks for the generous attribution.

    Commit ac15f5654aec8ae96fc814cbab331c580f790250 "test: Enable tool_bitcoin.py and interface_gui.py tests on Windows" happens to be the commit I feel is still I don't fully grasp, but it's more what it's referencing than what it's doing.

    What it's doing in the scope of interface_gui.py - instead of avoiding to run at all on Windows, only skip when the build is a vcpkg build. It references src/qt/test/CMakeLists.txt but that file is not immediately understandable to me. From looking at https://qt.developpez.com/doc/6.4/qguiapplication/#platformName-prop I think this is how I would make it more understandable:

    --- a/src/qt/test/CMakeLists.txt
    +++ b/src/qt/test/CMakeLists.txt
    @@ -40,12 +40,14 @@ add_test(NAME test_bitcoin-qt
       COMMAND test_bitcoin-qt
     )
     if(WIN32 AND VCPKG_TARGET_TRIPLET)
       set(plugin_path "$<SHELL_PATH:$<PATH:GET_PARENT_PATH,$<PATH:GET_PARENT_PATH,$<TARGET_PROPERTY:Qt6::QWindowsIntegrationPlugin,LOCATION_$<CONFIG>>>>>")
       set_property(TEST test_bitcoin-qt APPEND PROPERTY
         ENVIRONMENT_MODIFICATION
           # On Windows, vcpkg configures Qt with `-opengl dynamic`, which makes
    -      # the "minimal" platform plugin unusable due to internal Qt bugs.
    +      # the otherwise appropriate headless "minimal" platform plugin unusable
    +      # due to internal Qt bugs. So override to "windows" plugin in order to
    +      # make it work when built in that configuration (VCPKG_TARGET_TRIPLET).
           QT_QPA_PLATFORM=set:windows
           QT_PLUGIN_PATH=set:${plugin_path}
       )
       unset(plugin_path)
    

    My impression is that src/qt/test/CMakeLists.txt is supposed to make test_bitcoin-qt actually work, even when built with vcpkg.

    So why would you be changing the functional test to skip something which should be working?

    Well, when I build and run .\build\bin\Release\test_bitcoin-qt.exe I get:

    qt.qpa.plugin: Could not find the Qt platform plugin "windows" in ""
    This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem.
    

    Setting it to "minimal" has the same result.

    I tried printing the plugin path through adding this before the variable is unset:

      message("QT_PLUGIN_PATH=${plugin_path} (plugin_path)")
    

    and it prints:

    QT_PLUGIN_PATH=$<SHELL_PATH:$<PATH:GET_PARENT_PATH,$<PATH:GET_PARENT_PATH,$<TARGET_PROPERTY:Qt6::QWindowsIntegrationPlugin,LOCATION_$<CONFIG>>>>> (plugin_path)
    

    Manually setting:

    set QT_PLUGIN_PATH=C:\Users\hodlinator\bitcoin\build\vcpkg_installed\x64-windows\Qt6\plugins
    

    makes .\build\bin\Release\test_bitcoin-qt.exe run fine, both with minimal and windows plugins!

    So I wonder if the setting of QT_PLUGIN_PATH in CMakeLists.txt is somehow broken? I'm on CMake 4.4.2.

    Having the QT_PLUGIN_PATH set correctly also makes interface_gui.py run fine when modified to not skip!


    hodlinator commented at 7:31 PM on September 10, 2026:

    Yeah, didn't mean to put you in the same category as new-entrant sloppers. But I think it may be good to abide by the same rules or else alter/amend them.


    hebasto commented at 8:37 PM on September 10, 2026:

    FWIW, in the QML repo, we use the following code:

        file(GENERATE
          OUTPUT $<TARGET_FILE_DIR:bitcoin-qt>/qt.conf
          CONTENT "[Paths]\nPrefix = $<PATH:GET_PARENT_PATH,$<PATH:GET_PARENT_PATH,$<TARGET_FILE_DIR:Qt6::QWindowsIntegrationPlugin>>>\n"
        )
    

    This makes all targets runnable form the build tree on Windows.


    maflcko commented at 11:54 AM on September 17, 2026:

    It's important to me to distinguish words and ideas which are my own from those that come from other sources, so I use co-author tags for this.

    I think it is fine to do this, like it was done previously. See e.g. git log --grep="uggested by": 7209eb77902a8bf170fd7fbf6ec2388fec77b115 says: "Fix was suggested by willcl-ark in...", etc ...

    If you want to credit Claude for a specific noteworthy thing, I'd list that specific thing, and if the full commit was authored by Claude, you can say verbatim that it was authored by Claude, but also fully reviewed by you.

    I think that is clearer to everyone and hopefully addresses your needs. Otherwise, the co-author tags are only useful to you, but not really useful to anyone else.


    ryanofsky commented at 8:44 PM on September 23, 2026:

    re: #36105 (review)

    Thanks, added a regex to switch to prose attribution in future commits. But the policy does not make logical sense to me. I don't understand how an accurate Co-Authored-By: line communicates anything about how well an author understands a change. It seems like the policy is asking contributors to be less transparent and obfuscate the source of changes, maybe to avoid them being tagged in github. I'm not very interested in policies or policywriting though, so will leave deep thinking about this to others.


    ryanofsky commented at 9:54 PM on September 23, 2026:

    re: #36105 (review)

    Good findings here. For now I added the doc improvement. But @hebasto it seems like hodlinator found inconsistencies here that would be good to address:

    • QT_PLUGIN_PATH may not be set correctly, and if it is set correctly overriding QT_QPA_PLATFORM is not necessary?
    • The claim about 'the "minimal" platform plugin unusable due to internal Qt bugs' is inaccurate?

    (hodlinator's print message("QT_PLUGIN_PATH=${plugin_path} (plugin_path)") output is expected though because the variable contains generator expressions, which aren't expanded until cmake's "generate" phase after the configure phase where messages are printed. To see generator expressions you need to output them with something that runs during the generate phase like file(GENERATE OUTPUT <path> CONTENT "$<...>"))

    On getting the python test running in the native ci build, generating a qt.conf file as hebasto suggested seems like the best approach. Another approach would be for the python test or ci-windows.py to set the QT_PLUGIN_PATH environment variable themselves, but then they either have to derive that path themselves somehow, or get it from cmake. Getting it from cmake would not be easy because visual studio is a multi-config build system, so the value is not available until the generate phase, so it would need to rely on file(GENERATE) and have more code reading the generated file, basically reinventing the qt.conf approach while being more complicated. I think this is probably better to address in a separate pr since this PR is supposed be a bitcoin.exe bugfix, not change the way bitcoin-qt.exe is run.

  8. in src/bitcoin.cpp:214 in 09f3531588 outdated
     209 |          exec_args[0] = exe_path_str.c_str();
     210 |          if (util::ExecVp(exec_args[0], (char*const*)exec_args.data()) == -1) {
     211 | +#if defined(WIN32) && !defined(_UCRT)
     212 | +            // msvcrt's _spawnvp returns EINVAL (not ENOENT) for any missing
     213 | +            // file, even after ".exe" is appended. ucrt returns ENOENT.
     214 | +            if (allow_notfound && (errno == ENOENT || errno == EINVAL)) return false;
    


    hodlinator commented at 12:13 PM on August 31, 2026:

    Checked https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/spawnvp-wspawnvp?view=msvc-170 but this difference in error codes between runtimes is not documented there. Is it admitted elsewhere?


    ryanofsky commented at 2:14 AM on September 9, 2026:

    re: #36105 (review)

    Checked https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/spawnvp-wspawnvp?view=msvc-170 but this difference in error codes between runtimes is not documented there. Is it admitted elsewhere?

    It's not documented anywhere and was just seen in CI. I was meaning to follow up and get more information about this though, so I'll try to do that soon.


    ryanofsky commented at 4:13 PM on September 9, 2026:

    re: #36105 (review)

    this difference in error codes between runtimes is not documented there

    Followed up and this difference does not seem to be documented anywhere, only observed in CI. Did some more testing though and was able to simplify and write a better comment which is in the latest push


    hodlinator commented at 7:27 PM on September 10, 2026:

    cyb3ralbert commented at 5:21 AM on September 13, 2026:

    One more observation on this, from a windows-2022 runner with the cross-built binaries from the #36190 branch (which carries this PR's commits). When the target executable is missing, the error the user sees differs between runtimes:

    MSVCRT: Error: execvp failed to execute '...\bitcoind': Invalid argument
    UCRT:   Error: execvp failed to execute '...\bitcoind': No such file or directory
    

    The intermediate lookups in ExecCommand accept EINVAL as "not found" and move on, but the last attempt passes allow_notfound=false, so the raw errno goes into the std::system_error. Mapping EINVAL to ENOENT in that throw as well would make an MSVCRT build report a missing file the same way UCRT does, instead of "Invalid argument".


    ryanofsky commented at 10:04 PM on September 23, 2026:

    re: #36105 (review)

    Mapping EINVAL to ENOENT in that throw as well would make an MSVCRT build report a missing file the same way UCRT does, instead of "Invalid argument".

    When it comes to error reporting I think it's better to just to show actual error reported by operating system or runtime, and trying to translate one error code into another error code before printing it would not be good there. The earlier cases are using EINVAL for error handling not error reporting so they are a bit different.

  9. hodlinator commented at 12:16 PM on August 31, 2026: contributor

    Concept ACK 30ae05a21bdb30e63400385cf6d9dfd87dccfc70

  10. ryanofsky force-pushed on Sep 9, 2026
  11. ryanofsky commented at 2:18 AM on September 9, 2026: contributor

    <!-- begin push-9 -->

    Updated 30ae05a21bdb30e63400385cf6d9dfd87dccfc70 -> c02747ea3706e0c028b4be61a4dff054d65f3a3e (pr/bitwin.8 -> pr/bitwin.9, compare)<!-- end --> with exit code status fix and test from @Bortlesboat from https://github.com/bitcoin/bitcoin/pull/36190

  12. ryanofsky force-pushed on Sep 9, 2026
  13. ryanofsky commented at 2:38 PM on September 9, 2026: contributor

    <!-- begin push-10 -->

    Updated c02747ea3706e0c028b4be61a4dff054d65f3a3e -> 359a57d653406365a6158457e77ff926d6f58d0d (pr/bitwin.9 -> pr/bitwin.10, compare)<!-- end --> just dropping test commit 00fe470459ff36f22ef4a2e7d3e3e8903dac207b as requested

    <!-- begin push-11 -->

    Updated 359a57d653406365a6158457e77ff926d6f58d0d -> 5cfe9d873c7b3ca826467e33b6b650f43957d4e8 (pr/bitwin.10 -> pr/bitwin.11, compare)<!-- end --> to fix lint (unused os/shutil/subprocess imports in tool_bitcoin.py) https://github.com/bitcoin/bitcoin/actions/runs/34364703981/job/102510308140

    <!-- begin push-12 -->

    Updated 5cfe9d873c7b3ca826467e33b6b650f43957d4e8 -> 2b8779842a3fcb4afaa533a0490b9618ccc42c36 (pr/bitwin.11 -> pr/bitwin.12, compare)<!-- end --> simplifying MSVCRT workaround and documenting it better

  14. ryanofsky force-pushed on Sep 9, 2026
  15. DrahtBot added the label CI failed on Sep 9, 2026
  16. Bortlesboat referenced this in commit 57b837251e on Sep 9, 2026
  17. ryanofsky force-pushed on Sep 9, 2026
  18. DrahtBot removed the label CI failed on Sep 9, 2026
  19. hodlinator commented at 8:27 PM on September 10, 2026: contributor

    Reviewed 2b8779842a3fcb4afaa533a0490b9618ccc42c36

  20. hebasto commented at 5:09 PM on September 16, 2026: member

    There is a preliminary understanding among the build crowd that #33593 might land shortly after branching off "32.x", which renders the "bitcoin: Fix msvcrt regressions from _execvp to _spawnvp switch" commit unnecessary.

    #33593 has been merged now.

  21. cyb3ralbert commented at 12:35 PM on September 17, 2026: contributor

    Suggestion for the rebase on #33593: drop 68ae4a0f, keep 2b877984. They read as one story, but only the first one is MSVCRT-specific.

    #33593 did more than switch the release runtime: f5910d1a removed the Windows+MSVCRT cross-compile and native-test jobs, and b0781054 removed the remaining MSVCRT workarounds in the tree. Nothing the project builds or tests uses MSVCRT now, so the #if defined(WIN32) && !defined(_UCRT) branch in 68ae4a0f is no longer exercised in CI or releases. My own comment above about mapping EINVAL to ENOENT goes away with it, for the same reason.

    (depends still accepts the x86_64-w64-mingw32 triplet, and depends/README.md still offers it in the Nix cross-compile section, even though the "common triplets" list above it no longer does. If MSVCRT is really done, that line looks like a leftover.)

    2b877984 is a different bug. system_category on Windows interprets the value as a Win32 error code, while ExecVp sets a POSIX errno, and that is wrong on UCRT as well. It only looks right for the most common case, because ENOENT and ERROR_FILE_NOT_FOUND are both 2. On a windows-2022 runner, the same messages from MinGW libstdc++ and MSVC:

    errno        system_category                                                    generic_category
     2 ENOENT    The system cannot find the file specified                          No such file or directory
    13 EACCES    The data is invalid                                                Permission denied
    22 EINVAL    The device does not recognize the command                          Invalid argument
     8 ENOEXEC   Not enough memory resources are available to process this command  Exec format error
    

    A target that exists but cannot be executed sets EACCES. With the commit, the cross-built UCRT wrapper says:

    Error: execvp failed to execute '...\eacces\bitcoind': Permission denied
    

    Without it, that same case reads "The data is invalid".

    I rebased the branch on master to see what dropping 68ae4a0f costs: 6bce70ad and ac15f565 apply clean, and 2b877984 conflicts in exactly one place — the #if/#else block 68ae4a0f introduced — resolving to the single ENOENT line plus your comment. tool_bitcoin.py passes on Linux and on a native windows-2022 runner against the cross-built UCRT binaries.

  22. maflcko commented at 1:19 PM on September 17, 2026: member

    (depends still accepts the x86_64-w64-mingw32 triplet, and depends/README.md still offers it in the Nix cross-compile section, even though the "common triplets" list above it no longer does. If MSVCRT is really done, that line looks like a leftover.)

    See https://github.com/bitcoin/bitcoin/pull/36271

  23. test: Enable tool_bitcoin.py and interface_gui.py tests on Windows
    Now that util::ExecVp on Windows uses _spawnvp(_P_NOWAIT) + _cwait
    instead of _execvp, the bitcoin wrapper process blocks until the child
    exits and Python can capture its stdout/stderr and exit code normally.
    Remove the Windows skips added in #33229 and #35551.
    
    The interface_gui.py test is still skipped in vcpkg Qt builds. vcpkg builds Qt
    with -opengl dynamic, making the minimal platform plugin unusable due to
    internal Qt bugs. This matches existing logic in src/qt/test/CMakeLists.txt
    avoiding the minimal platform plugin with test_bitcoin-qt. A comment
    there is also updated for clarity.
    
    Co-Authored-By: Hodlinator <172445034+hodlinator@users.noreply.github.com>
    7fc08852e4
  24. bitcoin: Use generic_category for errno from exec/spawn CRT functions
    Switch from system_category to generic_category when throwing from
    ExecVp failure, so errno is read as a POSIX value. On Windows,
    system_category interprets codes as Win32 errors, so EINVAL=22 would
    produce "The device does not recognize the command" instead of
    "Invalid argument".
    
    This change was written with Claude Sonnet 4.6.
    ad98271f51
  25. ryanofsky force-pushed on Sep 23, 2026
  26. ryanofsky commented at 10:23 PM on September 23, 2026: contributor

    Thanks for the reviews!

    re: #36105 (comment) re: #36105 (comment)

    #33593 has been merged now. Suggestion for the rebase on #33593: drop 68ae4a0f, keep 2b877984

    Thanks dropped 68ae4a0f84b12587b9e2b53d3e306460fad4244a workaround. It still might be useful if backporting this change though, which I think could make sense because it is a bugfix.


    <!-- begin push-13 -->

    Updated 2b8779842a3fcb4afaa533a0490b9618ccc42c36 -> ad98271f512d89fb37cc1debe3a9319165210a22 (pr/bitwin.12 -> pr/bitwin.13, compare)<!-- end --> tweaking comments and dropping 68ae4a0f84b12587b9e2b53d3e306460fad4244a.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-01 05:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me