3

BACKGROUND
I am building a basic MFC application which helps me to run tests. The GUI is extremely spartaic. One button to START and another button to STOP the test, plus a static text object to display a few letters and digits indicating what stage the test is performing. This has been working satisfactorily for weeks.
I would find it useful if my application also made a video screen recording while the test is performed. The big plan is to compile FFmpeg library function calls into the code to implement this capability, but this will certainly take me weeks to learn, try and complete. In the meantime, a quick but sufficient solution is to call a precompiled ffmpeg.exe from a downloaded Windows binary build.
I added code to the ::OnBnClickedButtonStarttest() method to call ffmpeg.exe via CreateProcess(). This is working fine. The FFmpeg screen recording process starts in a new console window and does its intended job very well. When I select that console window and press Ctrl+C, the recording stops and I have the desired video file. I also added code to the ::OnBnClickedButtonStoptest() method to send the required Ctrl+C to the STDIN of the ffmpeg.exe process and finish recording when the STOP button is clicked. This is also working fine. MOST OF THE TIME.
These two portions of my source code are based on the example published at https://learn.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output by Microsoft.

PROBLEM
Stopping the screen recording process MOST OF THE TIME means that IT DOES NOT ALWAYS STOP. There are times when the solution does not work. And I am unable to find any reason why the same solution that works most of the time, happens to fail on other times. Eventually I would prefer to hide the FFmpeg console window and let it work behind the scene, but that would require a reliable way to stop the child process, and my current code proves to be not working reliably. Note: there is a #define BUFSIZE 128 line at the top of the source code.

DWORD dwNumOfBytesToWrite = 7;
DWORD dwWritten = 0;
CHAR chBuf[BUFSIZE] = { 0x03, 0x03, 0x03, 0x03, 0x03, 0x0A, 0x0D, 0x03, 0x0A, 0x0D };
BOOL bSuccess = FALSE;
bSuccess = WriteFile(g_hChildStd_IN_Wr, chBuf, dwNumOfBytesToWrite, &dwWritten, NULL);

Although sending a single Ctrl+C (0x03) should stop the ffmpeg process, I send a little more input to be sure. Still, for some reason I have to send the above WriteFile instruction twice, in order to actually see the process stop. I do not know why. This does not concern me much though, as long as the second time it does work.
What does concern me is the fact that there are times when the child process does not stop. No matter how many times I send it the above WriteFile line. I found another promising example. This time without anonymous pipes -which I suspect to be the source of the unreliable behaviour.
https://www.techpowerup.com/forums/threads/c-c-c-console-redirection-with-sockets-win32.62350/
This appear to use a named channel of its own to access the standard input of the child process. I prefer this approach. Unfortunately, it is not only that it does not work, it also does not compile. The Socket thisSocket; line refers to a non-existing type. There is no such type as Socket, not with an uppercase S and lowercase ocket. I tried to use all uppercase SOCKET type in its place, which does compile, but an accordingly modified WriteFile((HANDLE)thisSocket, chBuf, dwNumOfBytesToWrite, &dwWritten, NULL); line always returns FAILURE, and does not stop the child process. To make my investigation more difficult, a large number of internet search hits take me to WINSOCK related topics instead.

Did I miss something?
What would be the reliable way of sending a Ctrl+C to the ffmpeg.exe process, in order to tactfully ask it to finish recording?
Mind you, I can brutally kill the FFmpeg console window via TerminateProcess(g_ffmpeg_process_handle, 0), but that does not always allow the child to properly close the video file, resulting a damaged screen recording.

Keve
  • 383
  • 2
  • 13
  • 1
    Did you try closing the input pipe to signal the EOF condition? Something like [this](https://stackoverflow.com/questions/57042246/how-to-make-windows-pipe-add-an-eof-when-process-finishes). – rustyx Oct 12 '21 at 22:33
  • 1
    Instead of using `WriteFile()` to send a byte `0x03` to the STDIN of the spawned process, try using `GenerateConsoleCtrlEvent()` to send a CTRL-C signal to the console of the spawned process. See [Can I send a ctrl-C (SIGINT) to an application on Windows?](https://stackoverflow.com/questions/813086/) – Remy Lebeau Oct 12 '21 at 22:36
  • @rustyx: yes, I tried closing all the I/O handles pluss the process and thread handles. This made no difference. When the FFmpeg process does stop, it stops even while those handles are still open (and I close them after the process is stopped). And when the FFmpeg process does not stop, then closing those handles does not trigger the stop of the child process. I tried this by placing a third button, separating the "sending Ctrl+C" and "closing the handles" code portions. – Keve Oct 13 '21 at 12:04
  • @RemyLebeau: I experimented with that too, before trying WriteFile(0x03). Unfortunately `GenerateConsoleCtrlEvent()` never closed the child process. – Keve Oct 13 '21 at 12:06
  • How about using the new UWP API for screen capture instead? [Here's](https://github.com/MicrosoftDocs/SimpleRecorder/) a complete working example and [here's](https://blogs.windows.com/windowsdeveloper/2019/09/16/new-ways-to-do-screen-capture/) a blog about it. – rustyx Oct 13 '21 at 13:09
  • @rustyx: I have not ventured into UWP territory yet, consequently I did not know about this possibility. It is an unexpected suggestion. I am going to take a look. Thanks for pointing me towards this! I consider it as a Plan-B. In case I fail to find an acceptable solution for my current issue. – Keve Oct 13 '21 at 18:55
  • 1
    `Windows.Graphics.Capture` is **not** a UWP API. In fact, there isn't even such a thing as a UWP API. The UWP is a platform, that limits the available API surface for programs targeting that platform. The API itself is exposed as Windows Runtime types, most of which can be used in classic desktop applications. Rating use of a system service as a backup plan smells like a priority inversion issue. – IInspectable Oct 14 '21 at 08:56

1 Answers1

1

No proper solution, but I have an acceptable workaround.

I introduced a global variable ffmpChildProc_statusFlag with an initial value of ZERO.
When CreateProcess() successfully started the FFmpeg child process in its hidden Console Window, then I assign ffmpChildProc_statusFlag the value of ONE.
And I use RegisterWaitForSingleObject() to automatically launch a CALLBACK function when the FFmpeg child process actually terminates.

VOID CALLBACK onSubprocessExit(_In_ PVOID lpParameter, _In_ BOOLEAN TimerOrWaitFired) {
    //// Do clean-up by unregistering this callback instance.
    //// Update _ffmpChildProc_statusFlag_ to have a value of TWO.
}

These allow me to confidently know how well my efforts are working out.

I also wrote an EnumWindowsProc callback that uses GetWindowThreadProcessId() to return the process ID that belongs to the currently examined window, until I run into the process ID of my FFmpeg child process, which CreateProcess() returned in the dwProcessId member of its PROCESS_INFORMATION structure argument.
I could not find a better way to obtain the window handle of the created child process from its known process Id. So that is EnumWindowsUntilProcessHandleFound().

Then here is what I did

When the time to finish the FFmpeg child process has come, I first attempt the WriteFile(0x03) to the redirected StdIN pipe method shown in my question. I do that 1-5 times, each time checking the value of ffmpChildProc_statusFlag to see if I need to try one more time, having a few hundred milliseconds of pause after each attempt. Not closing the Write end of the pipe, so that the next write attempt can succeed.
If the child process has not yet ended, then I close the 3 handles, for

  • the Write-end of the pipe,
  • the child process,
  • the thread of the child process,

and again pause for some hundred milliseconds to allow time for the callback to complete (if the child process completes).
As a final attempt, if the FFmpeg process is still running, then I unhide the hidden console window of the child process, making it possible for the user to manually stop it with an actual Ctrl+C. But while at this last attempt, I try a few times to programatically input that Ctrl+C using SendInput().

CWnd* wFFmpPtr = CWnd::FromHandle(ffmpExe_window_handle);
if (!wFFmpPtr->IsWindowVisible()) {
    TRACE(L"\tREVEALING FFmp window.\n");
    wFFmpPtr->ShowWindow(SW_SHOWNORMAL);
} //// if not already visible
SetActiveWindow(ffmpExe_window_handle);
TRACE(L"\tSending Ctrl+C keyboard action to the frontmost window.\n");
INPUT vips;   //// My temporary virtual-input structure to define a keyboard key action.
vips.type = INPUT_KEYBOARD;   //// Setting up a generic keyboard event.
vips.ki.wScan = 0;   //// Ignoring the scan code, because the virtual-keycode will be provided instead.
vips.ki.time = 0;    //// Ignoring the timestamp, so the system will fill it for me.
vips.ki.dwExtraInfo = 0;   //// I have no extra info to provide.
//// Simulating a "Ctrl" key press.
vips.ki.wVk = 0x11;    //// Virtual-key code for the "Ctrl" key.
vips.ki.dwFlags = 0;   //// 0 means a normal key press (key going downwards).
SendInput(1, &vips, sizeof(INPUT));
//// Simulating a "C" key press.
vips.ki.wVk = 0x43;    //// Virtual-key code for the "C" key.
vips.ki.dwFlags = 0;
SendInput(1, &vips, sizeof(INPUT));
//// Simulating a "C" key release.
vips.ki.dwFlags = KEYEVENTF_KEYUP;   //// KEYEVENTF_KEYUP means a key release (key going upwards).
SendInput(1, &vips, sizeof(INPUT));
//// Simulating a "Ctrl" key release.
vips.ki.wVk = 0x11;    //// Virtual-key code for the "Ctrl" key.
SendInput(1, &vips, sizeof(INPUT));
TRACE(L"\tActive window should have received a Ctrl+C.\n");

By this point, the child window always closes. But, to be absolutely certain, I left there a conditional TerminateProcess() too -should that be called, it would likely end the child process with a damaged video recording.

The tried but failed approaches

GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);     //// Never worked.
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, 0); //// Never worked.
msgResult = wFFmp_ptr->SendMessage(WM_CHAR, 0x03, 1);    // Never worked.
msgResult = wFFmp_ptr->SendMessage(WM_UNICHAR, 0x03, 1); // Never worked.
msgResult = wFFmp_ptr->SendMessage(WM_KEYDOWN, 0x03, 1);  // Not these two.
msgResult = wFFmp_ptr->SendMessage(WM_KEYUP, 0x03, 1);
msgResult = wFFmp_ptr->SendMessage(WM_KEYDOWN, 0x11, 1); // Or these four either.
msgResult = wFFmp_ptr->SendMessage(WM_KEYDOWN, 0x43, 1);
msgResult = wFFmp_ptr->SendMessage(WM_KEYUP, 0x43, 1);
msgResult = wFFmp_ptr->SendMessage(WM_KEYUP, 0x11, 1);

None of these have ever managed to close the child process. Not once. Even though I had high hopes with them, because these would have directed the Ctrl+C signal to my particular window. Therefore, assumed to be more reliable. :-(

While the SendInput method does seem to work all the time, I am not happy with that approach because it sends the simulated keystrokes to the frontmost window, instead of a particular window. Hence it is more prone to errors. A mouse click or a real keyboard action of the user can activate another window, which then would receive the simulated Ctrl+C keystrokes intended for the FFmpeg console window.
For now, it is acceptable.

Keve
  • 383
  • 2
  • 13