2

I'm trying to write an app that would spawn a subprocess and communicate with it via standard output/input. Just to get the hang of it I tried writing a simple app that would send a message to the subprocess, and the subprocess would receive it and send it back. After frankly ridiculous amount of trial and error I managed to get a message to the subprocess, but I can't figure out how to send it back.

Here's my attempt:

#include <QApplication>
#include <QDataStream>
#include <QFile>
#include <QDebug>
#include <QProcess>
#include <QThread>

#define dumpval(x) qDebug()<<#x<<'='<<x

void slave()
{
    QApplication::setApplicationName("slave");
    qSetMessagePattern("%{appname}: %{message}");
    qDebug()<<"started";
    QFile input;
    QFile output;
    dumpval(input.open(stdin, QFile::ReadOnly));
    dumpval(output.open(stdout, QFile::WriteOnly));

    QObject::connect(&output, &QIODevice::bytesWritten, [](int bytesWritten){dumpval(bytesWritten);});

    QDataStream inputStream(&input);
    QDataStream outputStream(&output);

    QByteArray data;
    while (true){
        inputStream>>data;
        dumpval(data);
        if (!data.isEmpty()) break;
        inputStream.resetStatus();
        QThread::sleep(1);
    }

    dumpval(output.isWritable());
    outputStream<<data;
    dumpval(output.waitForBytesWritten(-1));

    qDebug()<<"data written";
    qDebug()<<"stopped";
}

void master(QString path)
{
    QApplication::setApplicationName("master");
    qSetMessagePattern("%{appname}: %{message}");
    qDebug()<<"started";
    QProcess p;
    QObject::connect(&p, &QIODevice::bytesWritten, [](int bytesWritten){dumpval(bytesWritten);});
    p.setProgram(path);
    p.setArguments({"slave"});
    p.setProcessChannelMode(QProcess::ForwardedErrorChannel);
    p.start();
    p.waitForStarted();

    QDataStream stream(&p);
    QByteArray data = "this is a test";
    stream<<data;
    dumpval(p.waitForBytesWritten(-1));

    data.clear();

    while (true){
        stream>>data;
        dumpval(data);
        if (!data.isEmpty()) break;
        stream.resetStatus();
        QThread::sleep(1);
    }

    qDebug()<<"stopped";
}

int main(int argc, char** argv)
{
    if (argc == 1) master(argv[0]);
    else slave();
}

And here's the output of this code:

master: started
master: bytesWritten = 18
master: p.waitForBytesWritten(-1) = true
master: data = ""
slave: started
slave: input.open(stdin, QFile::ReadOnly) = true
slave: output.open(stdout, QFile::WriteOnly) = true
slave: data = "this is a test"
slave: output.isWritable() = true
slave: output.waitForBytesWritten(-1) = false
slave: data written
slave: stopped
master: data = ""
master: data = ""
master: data = ""
master: data = ""
master: data = ""
master: data = ""
^C

What am I doing wrong?

user697683
  • 1,423
  • 13
  • 24

1 Answers1

3

QFile does not implement an asynchronous interface. The reads and writes are blocking, the waitForXxx methods are no-ops.

See this question for how to implement a non-blocking console I/O, should you wish to do so.

Since QFile is blocking, the slave() doesn't need a loop that checks the status.

You use QProcess using its blocking API, so the use of its signals is unnecessary. You also assume that reads will return complete chunks of data. Console I/O is stream oriented, not message oriented, thus you must use the QDataStream transactions to ensure that the reads have atomically succeeded. The readyRead indication merely indicates that some data is available. It could be just one byte.

If you'd wish to use a non-blocking stateful approach to dealing with QProcess and similar communications, see this answer for one approach.

Note that using argc[0] to start self as a slave is not reliable. Use QCoreApplication::applicationFilePath() instead.

The example below works, and produces the following output:

master: started
slave: started
slave: input.open(stdin, QFile::ReadOnly) = true
slave: output.open(stdout, QFile::WriteOnly) = true
slave: data = "this is a test\x00"
slave: data = ""
slave: inputStream.status() = 0
slave: stopped
master: data = "this is a test\x00"
master: data = ""
master: stopped
// https://github.com/KubaO/stackoverflown/tree/master/questions/process-echo-43523282
#include <QtCore>
#define dumpval(x) qDebug()<<#x<<'='<<x

void slave()
{
   QCoreApplication::setApplicationName("slave");
   qDebug()<<"started";
   QFile input, output;
   QDataStream inputStream{&input}, outputStream{&output};
   dumpval(input.open(stdin, QFile::ReadOnly));
   dumpval(output.open(stdout, QFile::WriteOnly));
   QByteArray data;
   do {
      inputStream >> data;
      outputStream << data;
      dumpval(data);
   } while (inputStream.status() == QDataStream::Ok && !data.isEmpty());
   dumpval(inputStream.status());
}

void master()
{
   QCoreApplication::setApplicationName("master");
   qDebug()<<"started";
   QProcess p;
   p.setProgram(QCoreApplication::applicationFilePath());
   p.setArguments({"slave"});
   p.setProcessChannelMode(QProcess::ForwardedErrorChannel);
   p.start();
   p.waitForStarted();

   QDataStream stream(&p);
   QByteArray data;
   stream << "this is a test" << QByteArray{};
   while (true) {
      stream.startTransaction();
      stream >> data;
      if (stream.commitTransaction()) {
         dumpval(data);
         if (data.isEmpty())
            break;
      } else
         p.waitForReadyRead();
   }
   p.waitForFinished();
}

int main(int argc, char** argv)
{
   QCoreApplication app(argc, argv);
   qSetMessagePattern("%{appname}: %{message}");
   if (app.arguments().size() < 2) master(); else slave();
   qDebug() << "stopped";
}
Community
  • 1
  • 1
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • How can I tell whether a given QOIDevice implements asynchronous interface? – user697683 Apr 21 '17 at 16:02
  • You generally can't. In Qt, `QFile` and `QBuffer` are synchronous, everything else is asynchronous (sockets and ports pretty much). For classes you write yourself or use from other libraries, you have to inspect the code to figure it out. – Kuba hasn't forgotten Monica Apr 21 '17 at 17:00