// 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 #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #else #include #include #include #include #endif #ifdef Q_OS_LINUX #include #endif #include 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 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 tryParseCommandLine(QCoreApplication &app); // Sets the working directory, returns a status code in case of error std::optional trySetWorkingDir(); // Reads the environment variables from the env file, returns a status code in case of error std::optional 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); if (!commandLineParser.value("wait").isEmpty()) { std::cout << commandLineParser.value("wait").toStdString(); std::cin.get(); } 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 } enum class Out { StdOut, StdErr }; void writeToOut(const QByteArray &data, Out out) { #ifdef Q_OS_WIN static const HANDLE outHandle = GetStdHandle(STD_OUTPUT_HANDLE); static const HANDLE errHandle = GetStdHandle(STD_ERROR_HANDLE); WriteFile(out == Out::StdOut ? outHandle : errHandle, data.constData(), data.size(), nullptr, nullptr); #else auto fp = out == Out::StdOut ? stdout : stderr; ::fwrite(data.constData(), 1, data.size(), fp); ::fflush(fp); #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 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 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.addOption( QCommandLineOption("wait", "Message to display to the user while waiting for key press", "waitmessage", "Press enter to continue ...")); 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 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); }