forked from qt-creator/qt-creator
Terminal: Enable TerminalProcessInterface
Adds a new helper app "process_stub" that replaces the previous. "process_stub_unix/win". The purpose was and is to allow processes to be "injected" into other hosts apps like terminals while still being able to control and debug them. A new base class called "TerminalInterface" is used for both the new Terminal plugin and the legacy TerminalProcess implementation. Fixes: QTCREATORBUG-16364 Change-Id: If21273fe53ad545d1a768c17c83db4bf2fd85395 Reviewed-by: Christian Stenger <christian.stenger@qt.io> Reviewed-by: Jarek Kobus <jaroslaw.kobus@qt.io> Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
541
src/tools/process_stub/main.cpp
Normal file
541
src/tools/process_stub/main.cpp
Normal file
@@ -0,0 +1,541 @@
|
||||
// Copyright (C) 2022 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QLocalSocket>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMutex>
|
||||
#include <QProcess>
|
||||
#include <QSocketNotifier>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QWinEventNotifier>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <optional>
|
||||
#include <signal.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <sys/ptrace.h>
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
|
||||
Q_LOGGING_CATEGORY(log, "qtc.process_stub", QtWarningMsg);
|
||||
|
||||
// Global variables
|
||||
|
||||
QCommandLineParser commandLineParser;
|
||||
|
||||
// The inferior command and arguments
|
||||
QStringList inferiorCmdAndArguments;
|
||||
// Whether to Suspend the inferior process on startup (to allow a debugger to attach)
|
||||
bool debugMode = false;
|
||||
// Whether to run in test mode (i.e. to start manually from the command line)
|
||||
bool testMode = false;
|
||||
// The control socket used to communicate with Qt Creator
|
||||
QLocalSocket controlSocket;
|
||||
// Environment variables to set for the inferior process
|
||||
std::optional<QStringList> environmentVariables;
|
||||
|
||||
QProcess inferiorProcess;
|
||||
int inferiorId{0};
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// A memory mapped helper to retrieve the pid of the inferior process in debugMode
|
||||
static int *shared_child_pid = nullptr;
|
||||
#endif
|
||||
|
||||
using OSSocketNotifier = QSocketNotifier;
|
||||
#else
|
||||
Q_PROCESS_INFORMATION *win_process_information = nullptr;
|
||||
using OSSocketNotifier = QWinEventNotifier;
|
||||
#endif
|
||||
// Helper to read a single character from stdin in testMode
|
||||
OSSocketNotifier *stdInNotifier;
|
||||
|
||||
QThread processThread;
|
||||
|
||||
// Helper to create the shared memory mapped segment
|
||||
void setupSharedPid();
|
||||
// Parses the command line, returns a status code in case of error
|
||||
std::optional<int> tryParseCommandLine(QCoreApplication &app);
|
||||
// Sets the working directory, returns a status code in case of error
|
||||
std::optional<int> trySetWorkingDir();
|
||||
// Reads the environment variables from the env file, returns a status code in case of error
|
||||
std::optional<int> readEnvFile();
|
||||
|
||||
void setupControlSocket();
|
||||
void setupSignalHandlers();
|
||||
void startProcess(const QString &program, const QStringList &arguments, const QString &workingDir);
|
||||
void readKey();
|
||||
void sendSelfPid();
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication a(argc, argv);
|
||||
|
||||
setupSharedPid();
|
||||
|
||||
auto error = tryParseCommandLine(a);
|
||||
if (error)
|
||||
return error.value();
|
||||
|
||||
qCInfo(log) << "Debug helper started: ";
|
||||
qCInfo(log) << "Socket:" << commandLineParser.value("socket");
|
||||
qCInfo(log) << "Inferior:" << inferiorCmdAndArguments.join(QChar::Space);
|
||||
qCInfo(log) << "Working Directory" << commandLineParser.value("workingDir");
|
||||
qCInfo(log) << "Env file:" << commandLineParser.value("envfile");
|
||||
qCInfo(log) << "Mode:"
|
||||
<< QLatin1String(testMode ? "test | " : "")
|
||||
+ QLatin1String(debugMode ? "debug" : "run");
|
||||
|
||||
error = trySetWorkingDir();
|
||||
if (error)
|
||||
return error.value();
|
||||
|
||||
error = readEnvFile();
|
||||
if (error)
|
||||
return error.value();
|
||||
|
||||
if (testMode) {
|
||||
sendSelfPid();
|
||||
setupSignalHandlers();
|
||||
|
||||
startProcess(inferiorCmdAndArguments[0],
|
||||
inferiorCmdAndArguments.mid(1),
|
||||
commandLineParser.value("workingDir"));
|
||||
|
||||
if (debugMode) {
|
||||
qDebug() << "Press 'c' to continue or 'k' to kill, followed by 'enter'";
|
||||
readKey();
|
||||
}
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
|
||||
setupControlSocket();
|
||||
|
||||
return a.exec();
|
||||
}
|
||||
|
||||
void sendMsg(const QByteArray &msg)
|
||||
{
|
||||
if (controlSocket.state() == QLocalSocket::ConnectedState) {
|
||||
controlSocket.write(msg);
|
||||
} else {
|
||||
qDebug() << "MSG:" << msg;
|
||||
}
|
||||
}
|
||||
|
||||
void sendPid(int inferiorPid)
|
||||
{
|
||||
sendMsg(QString("pid %1\n").arg(inferiorPid).toUtf8());
|
||||
}
|
||||
|
||||
void sendThreadId(int inferiorThreadPid)
|
||||
{
|
||||
sendMsg(QString("thread %1\n").arg(inferiorThreadPid).toUtf8());
|
||||
}
|
||||
|
||||
void sendSelfPid()
|
||||
{
|
||||
sendMsg(QString("spid %1\n").arg(QCoreApplication::applicationPid()).toUtf8());
|
||||
}
|
||||
|
||||
void sendExit(int exitCode)
|
||||
{
|
||||
sendMsg(QString("exit %1\n").arg(exitCode).toUtf8());
|
||||
}
|
||||
|
||||
void sendCrash(int exitCode)
|
||||
{
|
||||
sendMsg(QString("crash %1\n").arg(exitCode).toUtf8());
|
||||
}
|
||||
|
||||
void sendErrChDir()
|
||||
{
|
||||
sendMsg(QString("err:chdir %1\n").arg(errno).toUtf8());
|
||||
}
|
||||
|
||||
void doExit(int exitCode)
|
||||
{
|
||||
if (controlSocket.state() == QLocalSocket::ConnectedState && controlSocket.bytesToWrite())
|
||||
controlSocket.waitForBytesWritten(1000);
|
||||
|
||||
exit(exitCode);
|
||||
}
|
||||
|
||||
void onInferiorFinished(int exitCode, QProcess::ExitStatus status)
|
||||
{
|
||||
qCInfo(log) << "Inferior finished";
|
||||
|
||||
if (status == QProcess::CrashExit) {
|
||||
sendCrash(exitCode);
|
||||
doExit(exitCode);
|
||||
} else {
|
||||
sendExit(exitCode);
|
||||
doExit(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
void onInferiorErrorOccurered(QProcess::ProcessError error)
|
||||
{
|
||||
qCInfo(log) << "Inferior error: " << error << inferiorProcess.errorString();
|
||||
sendCrash(inferiorProcess.exitCode());
|
||||
doExit(1);
|
||||
}
|
||||
|
||||
void onInferiorStarted()
|
||||
{
|
||||
inferiorId = inferiorProcess.processId();
|
||||
qCInfo(log) << "Inferior started ( pid:" << inferiorId << ")";
|
||||
#ifdef Q_OS_WIN
|
||||
sendThreadId(win_process_information->dwThreadId);
|
||||
sendPid(inferiorId);
|
||||
#elif defined(Q_OS_DARWIN)
|
||||
// In debug mode we use the poll timer to send the pid.
|
||||
if (!debugMode)
|
||||
sendPid(inferiorId);
|
||||
#else
|
||||
ptrace(PTRACE_DETACH, inferiorId, 0, SIGSTOP);
|
||||
sendPid(inferiorId);
|
||||
#endif
|
||||
}
|
||||
|
||||
void setupUnixInferior()
|
||||
{
|
||||
#ifndef Q_OS_WIN
|
||||
if (debugMode) {
|
||||
qCInfo(log) << "Debug mode enabled";
|
||||
#ifdef Q_OS_DARWIN
|
||||
// We are using raise(SIGSTOP) to stop the child process, macOS does not support ptrace(...)
|
||||
inferiorProcess.setChildProcessModifier([] {
|
||||
// Let the parent know our pid ...
|
||||
*shared_child_pid = getpid();
|
||||
// Suspend ourselves ...
|
||||
raise(SIGSTOP);
|
||||
});
|
||||
#else
|
||||
// PTRACE_TRACEME will stop execution of the child process as soon as execve is called.
|
||||
inferiorProcess.setChildProcessModifier([] { ptrace(PTRACE_TRACEME, 0, 0, 0); });
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void setupWindowsInferior()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
inferiorProcess.setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments *args) {
|
||||
if (debugMode)
|
||||
args->flags |= CREATE_SUSPENDED;
|
||||
win_process_information = args->processInformation;
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void setupPidPollTimer()
|
||||
{
|
||||
#ifdef Q_OS_DARWIN
|
||||
if (!debugMode)
|
||||
return;
|
||||
|
||||
static QTimer pollPidTimer;
|
||||
|
||||
pollPidTimer.setInterval(1);
|
||||
pollPidTimer.setSingleShot(false);
|
||||
QObject::connect(&pollPidTimer, &QTimer::timeout, &pollPidTimer, [&] {
|
||||
if (*shared_child_pid) {
|
||||
qCInfo(log) << "Received pid during polling:" << *shared_child_pid;
|
||||
inferiorId = *shared_child_pid;
|
||||
sendPid(inferiorId);
|
||||
pollPidTimer.stop();
|
||||
munmap(shared_child_pid, sizeof(int));
|
||||
} else {
|
||||
qCDebug(log) << "Waiting for inferior to start...";
|
||||
}
|
||||
});
|
||||
pollPidTimer.start();
|
||||
#endif
|
||||
}
|
||||
void startProcess(const QString &executable, const QStringList &arguments, const QString &workingDir)
|
||||
{
|
||||
setupPidPollTimer();
|
||||
|
||||
qCInfo(log) << "Starting Inferior";
|
||||
|
||||
QObject::connect(&inferiorProcess,
|
||||
&QProcess::finished,
|
||||
QCoreApplication::instance(),
|
||||
&onInferiorFinished);
|
||||
QObject::connect(&inferiorProcess,
|
||||
&QProcess::errorOccurred,
|
||||
QCoreApplication::instance(),
|
||||
&onInferiorErrorOccurered);
|
||||
QObject::connect(&inferiorProcess,
|
||||
&QProcess::started,
|
||||
QCoreApplication::instance(),
|
||||
&onInferiorStarted);
|
||||
|
||||
inferiorProcess.setProcessChannelMode(QProcess::ForwardedChannels);
|
||||
if (!(testMode && debugMode))
|
||||
inferiorProcess.setInputChannelMode(QProcess::ForwardedInputChannel);
|
||||
inferiorProcess.setWorkingDirectory(workingDir);
|
||||
inferiorProcess.setProgram(executable);
|
||||
inferiorProcess.setArguments(arguments);
|
||||
|
||||
if (environmentVariables)
|
||||
inferiorProcess.setEnvironment(*environmentVariables);
|
||||
|
||||
setupWindowsInferior();
|
||||
setupUnixInferior();
|
||||
|
||||
inferiorProcess.start();
|
||||
}
|
||||
|
||||
std::optional<int> readEnvFile()
|
||||
{
|
||||
if (!commandLineParser.isSet("envfile"))
|
||||
return std::nullopt;
|
||||
|
||||
const QString path = commandLineParser.value("envfile");
|
||||
qCInfo(log) << "Reading env file: " << path << "...";
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qCWarning(log) << "Failed to open env file: " << path;
|
||||
return 1;
|
||||
}
|
||||
|
||||
environmentVariables = QStringList{};
|
||||
|
||||
while (!file.atEnd()) {
|
||||
QByteArray data = file.readAll();
|
||||
if (!data.isEmpty()) {
|
||||
for (const auto &line : data.split('\0')) {
|
||||
if (!line.isEmpty())
|
||||
*environmentVariables << QString::fromUtf8(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(log) << "Env: ";
|
||||
for (const auto &env : *environmentVariables)
|
||||
qCDebug(log) << env;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
void forwardSignal(int signum)
|
||||
{
|
||||
qCDebug(log) << "SIGTERM received, terminating inferior...";
|
||||
kill(inferiorId, signum);
|
||||
}
|
||||
#else
|
||||
static BOOL WINAPI ctrlHandler(DWORD dwCtrlType)
|
||||
{
|
||||
if (dwCtrlType == CTRL_C_EVENT || dwCtrlType == CTRL_BREAK_EVENT) {
|
||||
qCDebug(log) << "Terminate inferior...";
|
||||
inferiorProcess.terminate();
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
#endif
|
||||
|
||||
void setupSignalHandlers()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
SetConsoleCtrlHandler(ctrlHandler, TRUE);
|
||||
#else
|
||||
struct sigaction act;
|
||||
memset(&act, 0, sizeof(act));
|
||||
|
||||
act.sa_handler = SIG_IGN;
|
||||
if (sigaction(SIGTTOU, &act, NULL)) {
|
||||
qCWarning(log) << "sigaction SIGTTOU: " << strerror(errno);
|
||||
doExit(3);
|
||||
}
|
||||
|
||||
act.sa_handler = forwardSignal;
|
||||
if (sigaction(SIGTERM, &act, NULL)) {
|
||||
qCWarning(log) << "sigaction SIGTERM: " << strerror(errno);
|
||||
doExit(3);
|
||||
}
|
||||
|
||||
if (sigaction(SIGINT, &act, NULL)) {
|
||||
qCWarning(log) << "sigaction SIGINT: " << strerror(errno);
|
||||
doExit(3);
|
||||
}
|
||||
|
||||
qCDebug(log) << "Signals set up";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::optional<int> tryParseCommandLine(QCoreApplication &app)
|
||||
{
|
||||
commandLineParser.setApplicationDescription("Debug helper for QtCreator");
|
||||
commandLineParser.addHelpOption();
|
||||
commandLineParser.addOption(QCommandLineOption({"d", "debug"}, "Start inferior in debug mode"));
|
||||
commandLineParser.addOption(QCommandLineOption({"t", "test"}, "Don't start the control socket"));
|
||||
commandLineParser.addOption(
|
||||
QCommandLineOption({"s", "socket"}, "Path to the unix socket", "socket"));
|
||||
commandLineParser.addOption(
|
||||
QCommandLineOption({"w", "workingDir"}, "Working directory for inferior", "workingDir"));
|
||||
commandLineParser.addOption(QCommandLineOption({"v", "verbose"}, "Print debug messages"));
|
||||
commandLineParser.addOption(QCommandLineOption({"e", "envfile"}, "Path to env file", "envfile"));
|
||||
|
||||
commandLineParser.process(app);
|
||||
|
||||
inferiorCmdAndArguments = commandLineParser.positionalArguments();
|
||||
debugMode = commandLineParser.isSet("debug");
|
||||
testMode = commandLineParser.isSet("test");
|
||||
|
||||
if (!(commandLineParser.isSet("socket") || testMode) || inferiorCmdAndArguments.isEmpty()) {
|
||||
commandLineParser.showHelp(1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (commandLineParser.isSet("verbose"))
|
||||
QLoggingCategory::setFilterRules("qtc.process_stub=true");
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<int> trySetWorkingDir()
|
||||
{
|
||||
if (commandLineParser.isSet("workingDir")) {
|
||||
if (!QDir::setCurrent(commandLineParser.value("workingDir"))) {
|
||||
qCWarning(log) << "Failed to change working directory to: "
|
||||
<< commandLineParser.value("workingDir");
|
||||
sendErrChDir();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void setupSharedPid()
|
||||
{
|
||||
#ifdef Q_OS_DARWIN
|
||||
shared_child_pid = (int *) mmap(NULL,
|
||||
sizeof *shared_child_pid,
|
||||
PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_ANONYMOUS,
|
||||
-1,
|
||||
0);
|
||||
*shared_child_pid = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void onControlSocketConnected()
|
||||
{
|
||||
qCInfo(log) << "Connected to control socket";
|
||||
|
||||
sendSelfPid();
|
||||
setupSignalHandlers();
|
||||
|
||||
startProcess(inferiorCmdAndArguments[0],
|
||||
inferiorCmdAndArguments.mid(1),
|
||||
commandLineParser.value("workingDir"));
|
||||
}
|
||||
|
||||
void resumeInferior()
|
||||
{
|
||||
qCDebug(log) << "Continuing inferior... (" << inferiorId << ")";
|
||||
#ifdef Q_OS_WIN
|
||||
ResumeThread(win_process_information->hThread);
|
||||
#else
|
||||
kill(inferiorId, SIGCONT);
|
||||
#endif
|
||||
}
|
||||
|
||||
void killInferior()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
inferiorProcess.kill();
|
||||
#else
|
||||
kill(inferiorId, SIGKILL);
|
||||
#endif
|
||||
}
|
||||
|
||||
void onControlSocketReadyRead()
|
||||
{
|
||||
//k = kill, i = interrupt, c = continue, s = shutdown
|
||||
QByteArray data = controlSocket.readAll();
|
||||
for (auto ch : data) {
|
||||
qCDebug(log) << "Received:" << ch;
|
||||
|
||||
switch (ch) {
|
||||
case 'k': {
|
||||
qCDebug(log) << "Killing inferior...";
|
||||
killInferior();
|
||||
break;
|
||||
}
|
||||
#ifndef Q_OS_WIN
|
||||
case 'i': {
|
||||
qCDebug(log) << "Interrupting inferior...";
|
||||
kill(inferiorId, SIGINT);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case 'c': {
|
||||
resumeInferior();
|
||||
break;
|
||||
}
|
||||
case 's': {
|
||||
qCDebug(log) << "Shutting down...";
|
||||
doExit(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onControlSocketErrorOccurred(QLocalSocket::LocalSocketError socketError)
|
||||
{
|
||||
qCWarning(log) << "Control socket error:" << socketError;
|
||||
doExit(1);
|
||||
}
|
||||
|
||||
void setupControlSocket()
|
||||
{
|
||||
QObject::connect(&controlSocket, &QLocalSocket::connected, &onControlSocketConnected);
|
||||
QObject::connect(&controlSocket, &QLocalSocket::readyRead, &onControlSocketReadyRead);
|
||||
QObject::connect(&controlSocket, &QLocalSocket::errorOccurred, &onControlSocketErrorOccurred);
|
||||
|
||||
qCInfo(log) << "Waiting for connection...";
|
||||
controlSocket.connectToServer(commandLineParser.value("socket"));
|
||||
}
|
||||
|
||||
void onStdInReadyRead()
|
||||
{
|
||||
char ch;
|
||||
std::cin >> ch;
|
||||
if (ch == 'k') {
|
||||
killInferior();
|
||||
} else {
|
||||
resumeInferior();
|
||||
}
|
||||
}
|
||||
|
||||
void readKey()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
stdInNotifier = new QWinEventNotifier(GetStdHandle(STD_INPUT_HANDLE));
|
||||
#else
|
||||
stdInNotifier = new QSocketNotifier(fileno(stdin), QSocketNotifier::Read);
|
||||
#endif
|
||||
QObject::connect(stdInNotifier, &OSSocketNotifier::activated, &onStdInReadyRead);
|
||||
}
|
||||
Reference in New Issue
Block a user