Hold on to your saddles, this is a long one! Skip to "MCVE" part if you don't want to read everything.
I'm trying to make a process started with QProcess
exit gracefully. I do not control how the offending process exits, and it only accepts a Ctrl+C signal. What baffles me is that this sounds really simple and obvious to have in QProcess
's API. Yet, here I am :D
This is what I got so far:
Like I said, QProcess
does not really support this. So I have to dive into the Windows ecosystem and try to implement it natively. I found GenerateConsoleCtrlEvent
in Microsoft Docs. It seems like it does exactly what I need, so I tried using it. After some struggling with handling error messages in the Windows API, this is what I got:
QProcess myprocess = new QProcess(this);
myprocess->setReadChannel(QProcess::StandardOutput);
// I'm sorry that I have to be vague here. I can't really share this part.
myprocess->start("myexec", {"arg1", "arg2"});
//...
auto success = GenerateConsoleCtrlEvent(CTRL_C_EVENT, myprocess->pid()->dwProcessId);
if (!success) {
LPVOID lpMsgBuf;
auto err = GetLastError();
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
err,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
reinterpret_cast<LPTSTR>(&lpMsgBuf),
0, nullptr );
// probably could have used wcerr, but after making this work I was happy enough with it :D
auto error_string = QString::fromWCharArray((reinterpret_cast<LPTSTR>(lpMsgBuf)));
std::cerr << error_string.toStdString();
LocalFree(lpMsgBuf);
}
This just prints the handle is invalid.
to standard error. I kind of expected it, because the docs for GenerateConsoleCtrlEvent
say:
dwProcessGroupId [in]
The identifier of the process group to receive the signal. A process group is created when the CREATE_NEW_PROCESS_GROUP flag is specified in a call to the CreateProcess function. The process identifier of the new process is also the process group identifier of a new process group.
... and I was rooting for Qt to be already passing that flag in. This got me stuck for a while, and it seems to be the place where most questions about this here on SO (yes, I've seen them all - I think) seem to have died as well. Then I found QProcess::setCreateProcessArgumentsModifier
(With a nice example of usage here) which allows me to inject arguments into the CreateProcess
call. Then I updated my code to do this:
QProcess myprocess = new QProcess(this);
myprocess->setCreateProcessArgumentsModifier([this] (QProcess::CreateProcessArguments *args) {
args->flags |= CREATE_NEW_PROCESS_GROUP;
});
myprocess->setReadChannel(QProcess::StandardOutput);
myprocess->start("myexec", {"arg1", "arg2"});
//...
auto success = GenerateConsoleCtrlEvent(CTRL_C_EVENT, myprocess->pid()->dwProcessId);
if (!success) {
LPVOID lpMsgBuf;
auto err = GetLastError();
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
err,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
reinterpret_cast<LPTSTR>(&lpMsgBuf),
0, nullptr );
auto error_string = QString::fromWCharArray((reinterpret_cast<LPTSTR>(lpMsgBuf)));
std::cerr << error_string.toStdString();
LocalFree(lpMsgBuf);
}
This however gives me the same error (the handle is invalid
). From there, I tried other things, like injecting my own PROCESS_INFORMATION
struct to make sure I had the correct process identifier, or even adding CREATE_NEW_PROCESS_GROUP
to the lpStartupInfo
instead - what I now know to be the wrong place, as this caused some strange behavior (The asker in this link is not me :D)
Any ideas? Could I be doing this differently?
I'm using Qt 5.14.2, compiling with MSVC 2017 (64 bit).
MCVE
Making a "Minimal" MCVE for this is not easy :)
I have created a trivial windows application that handles Ctrl+C by simply printing a message. The goal is to make a QProcess trigger this handler, with no side effects. This is the source code for the child process:
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
#include "windows.h"
std::atomic<bool> should_stop = false;
BOOL WINAPI consoleHandler(DWORD signal) {
if (signal == CTRL_C_EVENT) {
std::cout << "\nThank you for your Ctrl+C event!\n";
should_stop.store(true);
}
return TRUE;
}
int main() {
if (!SetConsoleCtrlHandler(consoleHandler, TRUE)) {
std::cout << "Failed to set console handler\n";
return 1;
}
while (!should_stop) {
std::cout << "I'll keep printing this message until you stop me." << std::endl; // Yes, I want to flush every time.
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 0;
}
My "MVCE" for the parent application has a trivial main.cpp
, along with a ProcessHolder
class with a header and a source file. This is required so that I can have an event loop, and for Qt to be able to moc
the class properly (for use in said event loop).
main.cpp
#include <QCoreApplication>
#include <QTimer>
#include <memory>
#include "processholder.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
std::unique_ptr<ProcessHolder> ph(new ProcessHolder());
// Just so I can get the event loop running
QTimer::singleShot(0, ph.get(), &ProcessHolder::waitForInput);
return a.exec();
}
processholder.h
#ifndef PROCESSHOLDER_H
#define PROCESSHOLDER_H
#include <QObject>
#include <QProcess>
class ProcessHolder : public QObject
{
Q_OBJECT
public:
explicit ProcessHolder(QObject *parent = nullptr);
signals:
public slots:
void waitForInput();
private:
QProcess* p;
};
#endif // PROCESSHOLDER_H
processholder.cpp
#include "processholder.h"
#include <iostream>
#include <QTimer>
#include "Windows.h"
void tryFinishProcess(QProcess* p) {
auto success = GenerateConsoleCtrlEvent(CTRL_C_EVENT, p->pid()->dwProcessId);
if (!success) {
LPVOID lpMsgBuf;
auto err = GetLastError();
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr,
err,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
reinterpret_cast<LPTSTR>(&lpMsgBuf),
0, nullptr );
// probably could have used wcerr, but after making this work I was happy enough with it :D
auto error_string = QString::fromWCharArray((reinterpret_cast<LPTSTR>(lpMsgBuf)));
std::cerr << error_string.toStdString();
LocalFree(lpMsgBuf);
}
}
ProcessHolder::ProcessHolder(QObject *parent) : QObject(parent), p(new QProcess(this))
{
connect(p, &QProcess::readyReadStandardOutput, [this]() {
auto lines = p->readAllStandardOutput();
std::cout << lines.toStdString();
});
// Doing this for this example makes things fail miserably when trying to close the parent program.
// An when not doing it, the CtrlC event that is generated on tryFinishProcess actually ends the
// parent program, rather than the child one.
/*p->setCreateProcessArgumentsModifier([this] (QProcess::CreateProcessArguments *args) {
args->flags |= CREATE_NEW_PROCESS_GROUP;
});*/
std::cout << "starting process...\n";
p->start(R"(path\to\TrivialConsoleApp.exe)");
}
void ProcessHolder::waitForInput(){
char c;
bool quit = false;
// Print a small prompt just so we can differentiate input from output
std::cout << "> ";
if (std::cin >> c) {
switch(c) {
case 'k':
p->kill();
break;
case 't':
p->terminate();
break;
case 'c':
p->close();
break;
case 'g':
tryFinishProcess(p);
}
// any other character will just reliquinsh the hold on standard io for a small time, enough for the
// messages that were sent via cout to show up.
if (!quit) {
QTimer::singleShot(0, this, &ProcessHolder::waitForInput);
}
}
}
A few example runs:
Using QProcess::kill()
. Child process is terminated, but no CtrlC message.
Using tryFinishProcess
(see implementation above) actually made the parent process exit:
Again, using tryFinishProcess
, but this time with CREATE_NEW_PROCESS_GROUP
added (See comment on ProcessHolder
's constructor). A thing to note here is that pressing RETURN as asked by the terminal at the end does not work anymore (it does nothing), so something broke there:
My expectation for the three samples above (or at least for the last two) is to see a "Thank you for your Ctrl+C event!"
message (look at consoleHandler
on the child process) somewhere after asking for the process to finish. As it happens if I run it on console then press Ctrl+C: