Dušan Atanacković
Dušan Atanacković

Reputation: 444

Get response from QProcess in real time

I am new in c++ programming , so i need help with the logic (code would be awesome). I want to make QTextEdit to work as a terminal like widget. I am trying to achieve that with QProcess like:

void QPConsole::command(QString cmd){
    QProcess* process = new QProcess();
    connect(process, &QProcess::readyReadStandardOutput, [=](){ print(process->readAllStandardOutput()); });
    connect(process, &QProcess::readyReadStandardError, [=](){ print(process->readAllStandardError()); });
    connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),[=](int exitCode, QProcess::ExitStatus exitStatus){ prepareCommandLine(); return; });

    process->setProgram("/bin/bash");
    process->start();
    process->write(cmd.toStdString().c_str());
//    process->write("\n\r");

    process->closeWriteChannel();
}

The problem is next: If i don't close write channel (process->closeWriteChannel) than i am stuked in infinite loop. And if i do close it, than i cannot remember state (kinda makes new session) for example if i do "pwd" i get result, but if i do next "cd .." and then "pwd" the result is same as the first output of "pwd"

So, my question is is it possible to achieve some kind of real time output + having previous state remembered (like in real terminal) with QProcess or i have to do in some other way. In python i did it with subprocess calls with pipes, but i don't know what is the equilavent for c++.

An code example would be great. I have QTextEdit part (which is sending QString param to function), i need part with interaction with console.

Upvotes: 1

Views: 1041

Answers (1)

Antonio Dias
Antonio Dias

Reputation: 2881

The idea is simple: Keep an instance of QProcess and write commands to it followed by \n!

Here is a full example, with a QMainWindow and a Bash class, put it in main.cpp:

#include <QtCore>
#include <QtGui>
#include <QtWidgets>

class Bash : public QObject
{
    Q_OBJECT
public:
    explicit Bash(QObject *parent = nullptr) : QObject(parent)
    {

    }

    ~Bash()
    {
        closePrivate();
    }

signals:
    void readyRead(QString);

public slots:
    bool open(int timeout = -1)
    {
        if (m_bash_process)
            return false;

        m_timeout = timeout;

        return openPrivate();
    }

    void close()
    {
        closePrivate();
    }

    bool write(const QString &cmd)
    {
        return writePrivate(cmd);
    }

    void SIGINT()
    {
        SIGINTPrivate();
    }

private:
    //Functions
    bool openPrivate()
    {
        if (m_bash_process)
            return false;

        m_bash_process = new QProcess();

        m_bash_process->setProgram("/bin/bash");

        //Merge stdout and stderr in stdout channel
        m_bash_process->setProcessChannelMode(QProcess::MergedChannels);

        connect(m_bash_process, &QProcess::readyRead, this, &Bash::readyReadPrivate);
        connect(m_bash_process, QOverload<int>::of(&QProcess::finished), this, &Bash::closePrivate);

        m_bash_process->start();

        bool started = m_bash_process->waitForStarted(m_timeout);

        if (!started)
            m_bash_process->deleteLater();

        return started;
    }

    void closePrivate()
    {
        if (!m_bash_process)
            return;

        m_bash_process->closeWriteChannel();
        m_bash_process->waitForFinished(m_timeout);
        m_bash_process->terminate();
        m_bash_process->deleteLater();
    }

    void readyReadPrivate()
    {
        if (!m_bash_process)
            return;

        while (m_bash_process->bytesAvailable() > 0)
        {
            QString str = QLatin1String(m_bash_process->readAll());
            emit readyRead(str);
        }
    }

    bool writePrivate(const QString &cmd)
    {
        if (!m_bash_process)
            return false;

        if (runningPrivate())
            return false;

        m_bash_process->write(cmd.toLatin1());
        m_bash_process->write("\n");

        return m_bash_process->waitForBytesWritten(m_timeout);
    }

    void SIGINTPrivate()
    {
        if (!m_bash_process)
            return;

        QProcess::startDetached("pkill", QStringList() << "-SIGINT" << "-P" << QString::number(m_bash_process->processId()));
    }

    bool runningPrivate()
    {
        if (!m_bash_process)
            return false;

        QProcess process;
        process.setProgram("pgrep");
        process.setArguments(QStringList() << "-P" << QString::number(m_bash_process->processId()));
        process.setProcessChannelMode(QProcess::MergedChannels);

        process.start();

        process.waitForFinished(m_timeout);

        QString pids = QLatin1String(process.readAll());

        QStringList pid_list = pids.split(QRegularExpression("\n"), QString::SkipEmptyParts);

        return (pid_list.size() > 0);
    }

    //Variables
    QPointer<QProcess> m_bash_process;
    int m_timeout;
};

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        connect(&m_bash, &Bash::readyRead, this, &MainWindow::readyRead);

        QWidget *central_widget = new QWidget(this);

        QVBoxLayout *layout = new QVBoxLayout(central_widget);
        layout->addWidget(&m_message_board);
        layout->addWidget(&m_cmd_edit);
        layout->addWidget(&m_sigint_btn);

        m_message_board.setReadOnly(true);

        m_cmd_edit.setEnabled(false);

        m_sigint_btn.setText("Send SIGINT(CTRL+C)");

        connect(&m_cmd_edit, &QLineEdit::returnPressed, this, &MainWindow::writeToBash);

        connect(&m_sigint_btn, &QPushButton::clicked, this, &MainWindow::SIGINT);

        setCentralWidget(central_widget);

        resize(640, 480);

        QTimer::singleShot(0, this, &MainWindow::startBash);
    }

    ~MainWindow()
    {
        m_bash.close(); //Not mandatory, called by destructor
    }

private slots:
    void startBash()
    {
        //Open and give to each operation a maximum of 1 second to complete, use -1 to unlimited
        if (m_bash.open(1000))
        {
            m_cmd_edit.setEnabled(true);
            m_cmd_edit.setFocus();
        }
        else
        {
            QMessageBox::critical(this, "Error", "Failed to open bash");
        }
    }

    void writeToBash()
    {
        QString cmd = m_cmd_edit.text();

        m_cmd_edit.clear();

        if (!m_bash.write(cmd))
        {
            QMessageBox::critical(this, "Error", "Failed to write to bash");
        }
    }

    void readyRead(const QString &str)
    {
        m_message_board.appendPlainText(str);
    }

    void SIGINT()
    {
        m_bash.SIGINT();
    }

private:
    Bash m_bash;
    QPlainTextEdit m_message_board;
    QLineEdit m_cmd_edit;
    QPushButton m_sigint_btn;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

#include "main.moc"

Upvotes: 1

Related Questions