In Windows, attempt to use CreateProcess to launch [block|wallet|alert] notifications #5834

pull dfletcher wants to merge 3 commits into bitcoin:master from dfletcher:master changing 1 files +37 −1
  1. dfletcher commented at 11:16 PM on February 26, 2015: none

    Motivation: In Windows, ::system() causes a terminal console window to pop up. This window grabs the focus away from the user, an irritating interruption to workflow.

    Solution: Attempt to use CreateProcess() first. If this doesn't work for any reason, fall back to the previous solution and call ::system().

  2. in src/util.cpp:None in e1b25744c7 outdated
     692 | +    info.cb = sizeof(STARTUPINFO);
     693 | +    info.dwFlags = STARTF_USESHOWWINDOW;
     694 | +    info.wShowWindow = SW_HIDE;
     695 | +    PROCESS_INFORMATION processInfo;
     696 | +    ZeroMemory(&processInfo, sizeof(PROCESS_INFORMATION));
     697 | +    if (CreateProcess(NULL, &strCommand[0], NULL, NULL, false, 0, NULL, NULL, &info, &processInfo))
    


    laanwj commented at 4:43 PM on February 27, 2015:

    please use strCommand.c_str() (if you need zero-terminated) or strCommand.data() (if no zero terminator is necessary) i.s.o &strCommand[0]

  3. in src/util.cpp:None in e1b25744c7 outdated
     703 | +        CloseHandle(processInfo.hThread);
     704 | +        CloseHandle(processInfo.hProcess);
     705 | +        nErr = (int)nExitCode; // Had to pass a ULONG to GetExitCodeProcess().
     706 | +    }
     707 | +    else
     708 | +    {
    


    laanwj commented at 4:50 PM on February 27, 2015:

    Are you sure that this fallback is needed? I'm not sure what the implementation of mingw's system() does internally, but I'd assume it also makes use of CreateProcess or another win32 call.

  4. laanwj added the label Windows on Feb 27, 2015
  5. dfletcher commented at 5:58 PM on February 27, 2015: none

    Laanwj thanks for the feedback. Not quite sure why I did that &strCommand[0], I'll fix and reroll today. Regarding the second item, I believe system() handles more types of files, e.g. .bat files and document opening. I could be wrong about this. I will do some experimenting with both calls under MinGW and also possibly reroll that today.

  6. sipa commented at 10:58 AM on March 1, 2015: member

    Perhaps you can call ShellExecute, instead of CreateProcess?

  7. laanwj commented at 12:04 PM on March 9, 2015: member

    Yes, at least bat files would need to be supported. Document opening isn't that terribly important here, if one really wants to do that they could provide the program to launch it with in the command line.

  8. jgarzik commented at 1:58 PM on March 11, 2015: contributor

    Looks like a good start. Needs more testing.

  9. dfletcher commented at 9:24 PM on March 12, 2015: none

    Okay finally had some time to research this a little better. Thanks everyone here for the suggestions.

    I am not quite sure how to proceed, so here is what I have discovered, perhaps everyone here can make suggestions about the best route forward.

    Here's the gist of the program I used for testing. Edit: oh, right. don't forget to build using mingw with -mwindows flag.

    Edit2: Whoops my bad, Sleep takes milliseconds arg not seconds. Gist and table below updated, self exe is not always async. Thought that seemed funky. And really the "s" of the whole CreateProcess() column is because my code waits. We could change that behavior and be always async there.

    CreateProcess() ShellExecute() ::system()
    .bat script n s n a p s
    self exe n s n a p s
    pythonw.exe n s n a p s
    python.exe n s n a p s

    <sup>Key: p = popup, n = no popup, s = synchronous, a = asynchronous</sup>

    Each method has own strengths and weaknesses.

    CreateProcess()

    Pros

    • No popups.
    • Straightforward command implementation, pass whole command string to windows.

    Cons

    ShellExecute()

    Pros

    • Very consistent results while using SW_HIDE in the sixth argument of ShellExecute().
    • (Possible pro) users can change the SW_HIDE to SW_SHOW to change this behavior in a local build.

    Cons

    • Command must be parsed. The gist above only breaks on first whitespace, this would need to be smarter (accept a quoted string).

    ::system()

    Pros

    • Simple.

    Cons

    • Popups everywhere!

    Thoughts and opinions? Thanks again for the feedback!

  10. laanwj commented at 9:15 AM on March 13, 2015: member

    Seems to me that CreateProcess comes out best. Especially if it can work without fallback to ::system.

    Why this affects the non "W" version I have no idea. Is the fix to copy the string? Or to just cast c_str() to (char*)? Unsure.

    Just make a copy. Strings are not allowed to be mutated in-place in C++, but you could pass a begin_ptr(x) of a std::vector instead.

  11. dfletcher commented at 5:58 PM on March 13, 2015: none

    Ok updated. Nice call on begin_ptr() laanwj that makes it nice and clean. Since we removed the fallback wasn't quite sure what to do with nErr in that case, so just set it to -1 if CreateProcess() failed.

  12. kanzure commented at 6:03 PM on March 13, 2015: contributor

    Does the walletnotify hook need to be stress tested here? Sometimes there are quite a few created simultaneously.

  13. dfletcher commented at 6:17 PM on March 13, 2015: none

    @kanzure well it's anecdotal but I did just test 10 days of blocknotify with that last commit. Worked great, barely a blip on the CPU radar (admittedly fast 4 core modern machine). My test script this time is just a .bat piping the block hash to a file. It added 145 blocks to the output file really quickly and was not noticeably different from running it without the script. Would walletnotify be appreciably different from that? What's a good way to test it I wonder? Guess I could trash my blockchain and let it reload with a walletnotify script. Suggestions on what to put in it?

    Edit: oh hum I probably don't have enough tx in my testnet wallet to really "stress" test anything. Other ideas for approaches to this?

    Edit2: watchonly addresses I guess, could just add tons of them. Is this worthwhile?

    Edit3: Well I'm not really sure how to measure the results of this, so I'm leaning towards not testing walletnotify explicitly for performance. Intuition tells me this is a more efficient solution than ::system() just for the raw fact that Windows doesn't have to setup/teardown a window for each launch.

  14. dfletcher commented at 7:41 PM on March 13, 2015: none

    I'm confused why Travis failed here. Is that something in my fork? Did I screw up the merge from bitcoin/master?

  15. In Windows, attempt to use CreateProcess to launch [block|wallet|alert] notifications. 01087f77b6
  16. dfletcher commented at 8:27 PM on March 13, 2015: none

    Thanks a lot to jonasschnelli who helped me a ton in IRC figuring out how to work with github (this is my first pull request!)

  17. Testing if the reason for Travis failure is the removed fallback to ::system(). a14488ad61
  18. dfletcher commented at 11:34 PM on March 13, 2015: none

    @laanwj It seems that the fallback was necessary for the case of calling a terminal command like the "echo" in this test case (which is what fails w commit 01087f7 above).

    I can imagine one other solution to this which I just tested with my gist (updated) locally. We can prepend C:\Windows\System32\cmd.exe /C to the user command and force it to execute in a shell. Of course, the code would need to be better than just hard coding C:\Windows.

    tl;dr: The two choices I can think of is either use the fallback solution or call an interpreter by hand.

    Other ideas?

    Edit: oh I'd just like to add, using the interpreter actually works very nicely. No popup and it pipes to file as expected. My vote on how to implement this is heading in that direction.

    Edit2: just updated the gist to use GetSystemDirectory(). Could work. Edit3: gist makes a vector. Edit4: it now does that in a nicer way using a stringstream. Edit5: Travis seems okay with this solution :+1:

  19. Testing explicit interpreter method on Travis. 90c1d35e39
  20. dfletcher commented at 6:28 PM on March 16, 2015: none

    Still no opinions? Well it's easy for me to change this code to go either route, fallback or call cmd.exe. I would really like to have some solution for this even if it still pops up the window in some cases (the fallback solution).

  21. laanwj commented at 12:25 PM on March 19, 2015: member

    @dfletcher It's IMO not necessary to support providing a terminal command directly. As I said, what needs to be supported are .exe's and .bat's. The rest can be done from there.

    As said, if remotely possible there should only be one path and no chain of fallbacks. If it fails, it fails. And simple code is preferable to complex code.

  22. dfletcher commented at 5:31 PM on March 19, 2015: none

    @laanwj there is a test in the system which is testing using echo. This fails if we do not provide a fallback or use the cmd.exe interpreter directly. See the Travis failure above + two successes afterwards, failure uses no fallback or interpreter, first success is just putting the fallback back, second success is with an interpreter.

    So that leaves (1) Use fallback. (2) Use an interpreter. (3) Remove this test that uses "echo" which is not an exe it's an interpreter command.

    You seem to be suggesting #3 which doesn't sound good to me. Changing the test conditions because of a difference in the API calls we're making seems backwards to me.

  23. gavinandresen commented at 5:38 PM on March 19, 2015: contributor

    -alertnotify is a bitcoind, not Bitcoin-Qt, feature-- I wouldn't expect people running Bitcoin-Qt to run with -alertnotify, they're notified of alerts through the GUI.

    So I think I'm NACK on this pull request in general for solving a problem that I don't think needs solving (command window popping up if you -alertnotify with Bitcoin-Qt).

  24. dfletcher commented at 5:40 PM on March 19, 2015: none

    @gavinandresen It solves -blocknotify and -walletnotify popping up a window also. -alertnotify is incidental.

    Edit hum maybe I'm not being super clear about this. All three of those functions use the same utility function runCommand(). The fact that that test is checking -alertnotify is not exactly important. What is important is that I believe whoever wrote that test wants to make sure that the command processor can handle a command like "echo %s >> poo.txt" which is a perfectly reasonable implementation of any of the notifies, stuff it in a file and read it back later in some other process.

  25. in src/util.cpp:None in 90c1d35e39
     712 | +    char system32[256];
     713 | +    if (GetSystemDirectory(system32, 255)) {
     714 | +        std::stringstream ssCommand;
     715 | +        ssCommand << system32 << "\\cmd.exe /C " << strCommand;
     716 | +        std::string strTempCmd = ssCommand.str();
     717 | +        std::vector<char> vecCommand(strTempCmd.begin(), strTempCmd.end() + 1);
    


    laanwj commented at 11:15 AM on April 15, 2015:

    It is not allowed to use end()+1: end() is the end of the data. There is guarantee that there is anything after it, let alone a \0 byte. Only c_str guarantees a zero-terminated string.

    This is becoming quite ugly, but I guess what you could do is (this gets rid of the stringstream too)

    std::string strTempCmd = std::string(system32) + "\\cmd.exe /C " + strCommand;
    const char *sTempCmd = ssCommand.c_str();
    std::vector<char> vecCommand(sTempCmd, sTempCmd + strlen(sTempCmd) + 1);
    

    laanwj commented at 11:19 AM on April 15, 2015:

    OTOH I don't see anywhere in the CreateProcess documentation that it will actually try to change the passed-in string. So this should suffice:

    std::string strTempCmd = std::string(system32) + "\\cmd.exe /C " + strCommand;
    if (CreateProcess(NULL, const_cast<char*>(strTempCmd.c_str()), NULL, NULL, false, 0, NULL, NULL, &info, &processInfo))
    
  26. in src/util.cpp:None in 90c1d35e39
     713 | +    if (GetSystemDirectory(system32, 255)) {
     714 | +        std::stringstream ssCommand;
     715 | +        ssCommand << system32 << "\\cmd.exe /C " << strCommand;
     716 | +        std::string strTempCmd = ssCommand.str();
     717 | +        std::vector<char> vecCommand(strTempCmd.begin(), strTempCmd.end() + 1);
     718 | +        if (CreateProcess(NULL, begin_ptr(vecCommand), NULL, NULL, false, 0, NULL, NULL, &info, &processInfo))
    


    laanwj commented at 11:15 AM on April 15, 2015:

    Code style: we have { on the same line as the if/else.

  27. laanwj commented at 11:21 AM on April 15, 2015: member

    I do tend to agree with @gavinandresen that it'not s terribly useful to use -*notify with the GUI. For the daemon bitcoind.exe this is not an issue as it has its own shell window so should never pop up a new one?

  28. laanwj commented at 8:19 AM on June 10, 2015: member

    Closing due to inactivity. There are still some comments open. Let me know when you start working on this again, and I'll reopen.

  29. laanwj closed this on Jun 10, 2015

  30. MarcoFalke locked this on Sep 8, 2021

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-04-13 18:15 UTC

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