Заметки по языку с и с++

QProcess: запуск внешних программ и обмен данными через stdin/stdout

Введение

Многие приложения должны взаимодействовать с внешними программами: запускать утилиты командной строки, конвертеры, компиляторы, скрипты. В Qt для этого есть класс QProcess, который запускает внешний процесс и предоставляет асинхронный интерфейс для отправки данных в его стандартный ввод и чтения из стандартного вывода и потока ошибок — по аналогии с QIODevice, что делает работу с дочерним процессом похожей на работу с файлом или сокетом.

Концепция

QProcess::start() запускает программу асинхронно; о завершении процесса сигнализирует finished(int exitCode, QProcess::ExitStatus exitStatus), а о готовности новых данных в stdout/stderr — сигналы readyReadStandardOutput()/readyReadStandardError(). Запись в стандартный ввод дочернего процесса осуществляется через write(), как и для любого QIODevice.

Важная деталь — каналы вывода по умолчанию разделены (SeparateChannels): нужно отдельно читать readAllStandardOutput() и readAllStandardError(). Если важен только общий вывод без разделения на потоки, можно объединить их через setProcessChannelMode(QProcess::MergedChannels).

Пример кода

#include <QCoreApplication>
#include <QProcess>
#include <QDebug>

class ProcessRunner : public QObject
{
    Q_OBJECT
public:
    void runCommand(const QString &program, const QStringList &arguments)
    {
        connect(&m_process, &QProcess::readyReadStandardOutput, this, [this]() {
            qDebug().noquote() << "stdout:" << m_process.readAllStandardOutput();
        });
        connect(&m_process, &QProcess::readyReadStandardError, this, [this]() {
            qWarning().noquote() << "stderr:" << m_process.readAllStandardError();
        });
        connect(&m_process, &QProcess::errorOccurred, this, [](QProcess::ProcessError error) {
            qWarning() << "Ошибка запуска процесса:" << error;
        });
        connect(&m_process, &QProcess::finished, this,
                [this](int exitCode, QProcess::ExitStatus status) {
            qDebug() << "Процесс завершён, код:" << exitCode
                     << "статус:" << (status == QProcess::NormalExit ? "обычный" : "сбой");
            emit finished();
        });

        m_process.start(program, arguments);
    }

    // Пример интерактивного взаимодействия через stdin
    void sendInput(const QString &text)
    {
        m_process.write((text + "n").toUtf8());
    }

signals:
    void finished();

private:
    QProcess m_process;
};

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    ProcessRunner runner;
    QObject::connect(&runner, &ProcessRunner::finished, &app, &QCoreApplication::quit);

    // Пример: запуск python-скрипта, читающего строку из stdin
    runner.runCommand("python3", {"-c", "print(input('Echo: '))"});

    QTimer::singleShot(200, [&runner]() {
        runner.sendInput("Привет из QProcess");
    });

    return app.exec();
}

#include "main.moc"

Пояснения к коду

ProcessRunner подписывается на все ключевые сигналы QProcess перед вызовом start(): вывод (readyReadStandardOutput/readyReadStandardError), ошибки запуска (errorOccurred, срабатывает, например, если исполняемый файл не найден) и завершение (finished, с отдельными кодом возврата и статусом завершения). Метод sendInput() показывает, как писать в стандартный ввод запущенного процесса — это работает, только если сам процесс ожидает ввод (как в примере с input() в Python).

Обратите внимание на использование .noquote() при логировании вывода процесса — без него qDebug() оборачивает QByteArray в кавычки и экранирует управляющие символы, что неудобно при выводе многострочного текста.

Подводные камни

  • Путь к исполняемому файлу не найден в PATH на целевой машине. Жёстко прописанный относительный путь к программе, работающий на машине разработчика, может не сработать в продакшен-окружении — лучше искать исполняемый файл через QStandardPaths::findExecutable() или явно указывать полный путь, разворачиваемый в зависимости от ОС.
  • Дедлок из-за переполнения буфера stdout/stderr. Если дочерний процесс производит много вывода, а приложение не читает его своевременно (например, ждёт waitForFinished() без параллельного чтения), внутренний буфер ОС может заполниться, и дочерний процесс заблокируется на попытке писать дальше — это классический deadlock при синхронном взаимодействии с дочерними процессами.
  • waitForFinished() без таймаута может зависнуть навечно, если дочерний процесс зависает сам или ждёт ввода, который никогда не придёт — всегда указывайте таймаут и обрабатывайте случай, когда процесс не завершился (process.kill()).
  • Кодировка вывода процесса. Вывод стандартных консольных утилит на Windows часто в кодировке CP866/CP1251, а не UTF-8 — прямое преобразование QByteArray в QString без учёта реальной кодировки даёт «кракозябры»; нужно явно конвертировать через QStringDecoder или соответствующий codec с указанием правильной кодировки источника.