Refactor CallerHandle / LauncherHandle

Fix a race condition in the following scenario:

  QtcProcess proc(...)
  ...
  proc.start();
  proc.waitForStarted();
  if (!proc.waitForFinished()) {
      // handle failure here
      return;
  }
  // handle success

Move all the data into the caller's handle
and manage the QtcProcess state only from
inside caller's thread. This eliminates race
conditions when state changed from inside launcher's
thread while the caller's thread isn't notified
immediately.

For example: currently, when the launcher's thread receives
finished signal it doesn't change the process state
immediately, but posts a finished signal to be
dispatched in the caller's thread. When the caller's
thread dispatches the posted signal (inside flush() method)
it changes its state and posts the finished signal to the
outside world.

Don't flush all signals from waitForStarted(). Flush
only started signal in this case.

Change-Id: Ia39c4021bf43b8d0e8fcda789c367c096bfd032c
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Jarek Kobus
2021-08-23 17:58:44 +02:00
parent d932b8535a
commit 1a5db9ca4e
4 changed files with 750 additions and 630 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -34,8 +34,9 @@
#include <QProcess> #include <QProcess>
#include <QWaitCondition> #include <QWaitCondition>
#include <vector>
#include <atomic> #include <atomic>
#include <memory>
#include <vector>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QLocalSocket; class QLocalSocket;
@@ -45,13 +46,16 @@ namespace Utils {
namespace Internal { namespace Internal {
class LauncherInterfacePrivate; class LauncherInterfacePrivate;
class CallerHandle; class LauncherHandle;
class LauncherSignal;
class ErrorSignal;
class StartedSignal;
class ReadyReadSignal;
class FinishedSignal;
// Moved to the launcher thread, returned to caller's thread. // All the methods and data fields in this class are called / accessed from the caller's thread.
// It's assumed that this object will be alive at least // Exceptions are explicitly marked.
// as long as the corresponding QtcProcess is alive. class CallerHandle : public QObject
class LauncherHandle : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
@@ -63,14 +67,21 @@ public:
Finished Finished
}; };
Q_ENUM(SignalType) Q_ENUM(SignalType)
CallerHandle(quintptr token, ProcessMode mode) : QObject(), m_token(token), m_processMode(mode) {}
// All the public methods in this class are called exclusively from the caller's thread. LauncherHandle *launcherHandle() const { return m_launcherHandle; }
bool waitForStarted(int msecs) void setLauncherHandle(LauncherHandle *handle) { QMutexLocker locker(&m_mutex); m_launcherHandle = handle; }
{ return waitForSignal(msecs, SignalType::Started); }
bool waitForReadyRead(int msces) bool waitForStarted(int msecs);
{ return waitForSignal(msces, SignalType::ReadyRead); } bool waitForReadyRead(int msces);
bool waitForFinished(int msecs) bool waitForFinished(int msecs);
{ return waitForSignal(msecs, SignalType::Finished); }
// Returns the list of flushed signals.
QList<SignalType> flush();
QList<SignalType> flushFor(SignalType signalType);
bool shouldFlushFor(SignalType signalType) const;
// Called from launcher's thread exclusively.
void appendSignal(LauncherSignal *launcherSignal);
QProcess::ProcessState state() const; QProcess::ProcessState state() const;
void cancel(); void cancel();
@@ -83,6 +94,8 @@ public:
void setErrorString(const QString &str); void setErrorString(const QString &str);
void start(const QString &program, const QStringList &arguments, const QByteArray &writeData); void start(const QString &program, const QStringList &arguments, const QByteArray &writeData);
// Called from caller's or launcher's thread.
void startIfNeeded();
qint64 write(const QByteArray &data); qint64 write(const QByteArray &data);
@@ -105,46 +118,20 @@ signals:
void finished(int exitCode, QProcess::ExitStatus status); void finished(int exitCode, QProcess::ExitStatus status);
void readyReadStandardOutput(); void readyReadStandardOutput();
void readyReadStandardError(); void readyReadStandardError();
private: private:
bool waitForSignal(int msecs, CallerHandle::SignalType newSignal);
bool canWaitFor(SignalType newSignal) const; // TODO: employ me before calling waitForSignal()
// Called from caller's thread exclusively. // Called from caller's or launcher's thread. Call me with mutex locked.
bool waitForSignal(int msecs, SignalType newSignal);
bool doWaitForSignal(int msecs, SignalType newSignal);
bool canWaitFor(SignalType newSignal) const;
// Called from caller's or launcher's thread.
void doStart(); void doStart();
// Called from caller's or launcher's thread.
void sendPacket(const Internal::LauncherPacket &packet);
// Called from caller's or launcher's thread.
bool isCalledFromCallersThread() const;
// Called from caller's or launcher's thread. Call me with mutex locked.
bool isCalledFromLaunchersThread() const;
// Called from caller's thread exclusively.
void slotErrorOccurred();
void slotStarted();
void slotReadyRead();
void slotFinished();
// Called from caller's thread, moved to launcher's thread.
LauncherHandle(quintptr token, ProcessMode mode) : m_token(token), m_processMode(mode) {}
// Called from caller's thread exclusively.
void createCallerHandle();
void destroyCallerHandle();
// Called from launcher's thread exclusively.
void flushCaller();
void handlePacket(LauncherPacketType type, const QByteArray &payload);
void handleErrorPacket(const QByteArray &packetData);
void handleStartedPacket(const QByteArray &packetData);
void handleReadyReadStandardOutput(const QByteArray &packetData);
void handleReadyReadStandardError(const QByteArray &packetData);
void handleFinishedPacket(const QByteArray &packetData);
// Called from launcher's thread exclusively.
void handleSocketReady();
void handleSocketError(const QString &message);
// Called from launcher's thread exclusively.
void wakeUpIfWaitingFor(SignalType newSignal);
// Called from caller's thread exclusively.
QByteArray readAndClear(QByteArray &data) const QByteArray readAndClear(QByteArray &data) const
{ {
const QByteArray tmp = data; const QByteArray tmp = data;
@@ -152,22 +139,24 @@ private:
return tmp; return tmp;
} }
// Called from caller's or launcher's thread. void handleError(const ErrorSignal *launcherSignal);
void sendPacket(const Internal::LauncherPacket &packet); void handleStarted(const StartedSignal *launcherSignal);
void handleReadyRead(const ReadyReadSignal *launcherSignal);
void handleFinished(const FinishedSignal *launcherSignal);
// Called from caller's or launcher's thread. // Lives in launcher's thread. Modified from caller's thread.
bool isCalledFromLaunchersThread() const; LauncherHandle *m_launcherHandle = nullptr;
bool isCalledFromCallersThread() const;
mutable QMutex m_mutex; mutable QMutex m_mutex;
QWaitCondition m_waitCondition; // Accessed from caller's and launcher's thread
QList<LauncherSignal *> m_signals;
const quintptr m_token; const quintptr m_token;
const ProcessMode m_processMode; const ProcessMode m_processMode;
SignalType m_waitingFor = SignalType::NoSignal;
QProcess::ProcessState m_processState = QProcess::NotRunning; // Modified from caller's thread, read from launcher's thread
// cancel() sets it to false, modified only in caller's thread, don't need to be protected by mutex std::atomic<QProcess::ProcessState> m_processState = QProcess::NotRunning;
bool m_awaitingShouldContinue = false; std::unique_ptr<StartProcessPacket> m_startPacket;
int m_processId = 0; int m_processId = 0;
int m_exitCode = 0; int m_exitCode = 0;
QProcess::ExitStatus m_exitStatus = QProcess::ExitStatus::NormalExit; QProcess::ExitStatus m_exitStatus = QProcess::ExitStatus::NormalExit;
@@ -175,7 +164,6 @@ private:
QByteArray m_stderr; QByteArray m_stderr;
QString m_errorString; QString m_errorString;
QProcess::ProcessError m_error = QProcess::UnknownError; QProcess::ProcessError m_error = QProcess::UnknownError;
bool m_socketError = false;
QString m_command; QString m_command;
QStringList m_arguments; QStringList m_arguments;
@@ -185,15 +173,67 @@ private:
QProcess::ProcessChannelMode m_channelMode = QProcess::SeparateChannels; QProcess::ProcessChannelMode m_channelMode = QProcess::SeparateChannels;
QString m_standardInputFile; QString m_standardInputFile;
// Lives in caller's thread.
CallerHandle *m_callerHandle = nullptr;
bool m_belowNormalPriority = false; bool m_belowNormalPriority = false;
QString m_nativeArguments; QString m_nativeArguments;
bool m_lowPriority = false; bool m_lowPriority = false;
bool m_unixTerminalDisabled = false; bool m_unixTerminalDisabled = false;
};
friend class LauncherSocket; // Moved to the launcher thread, returned to caller's thread.
// It's assumed that this object will be alive at least
// as long as the corresponding QtcProcess is alive.
class LauncherHandle : public QObject
{
Q_OBJECT
public:
// Called from caller's thread, moved to launcher's thread afterwards.
LauncherHandle(quintptr token, ProcessMode mode) : m_token(token) {}
// Called from caller's thread exclusively.
bool waitForSignal(int msecs, CallerHandle::SignalType newSignal);
CallerHandle *callerHandle() const { return m_callerHandle; }
void setCallerHandle(CallerHandle *handle) { QMutexLocker locker(&m_mutex); m_callerHandle = handle; }
// Called from caller's thread exclusively.
void setCanceled() { m_awaitingShouldContinue = false; }
// Called from launcher's thread exclusively.
void handleSocketReady();
void handleSocketError(const QString &message);
void handlePacket(LauncherPacketType type, const QByteArray &payload);
// Called from caller's thread exclusively.
bool isSocketError() const { return m_socketError; }
private:
// Called from caller's thread exclusively.
bool doWaitForSignal(int msecs, CallerHandle::SignalType newSignal);
// Called from launcher's thread exclusively. Call me with mutex locked.
void wakeUpIfWaitingFor(CallerHandle::SignalType newSignal);
// Called from launcher's thread exclusively. Call me with mutex locked.
void flushCaller();
// Called from launcher's thread exclusively.
void handleErrorPacket(const QByteArray &packetData);
void handleStartedPacket(const QByteArray &packetData);
void handleReadyReadStandardOutput(const QByteArray &packetData);
void handleReadyReadStandardError(const QByteArray &packetData);
void handleFinishedPacket(const QByteArray &packetData);
// Called from caller's or launcher's thread.
bool isCalledFromLaunchersThread() const;
bool isCalledFromCallersThread() const;
// Lives in caller's thread. Modified only in caller's thread. TODO: check usages - all should be with mutex
CallerHandle *m_callerHandle = nullptr;
// Modified only in caller's thread.
bool m_awaitingShouldContinue = false;
mutable QMutex m_mutex;
QWaitCondition m_waitCondition;
const quintptr m_token;
std::atomic_bool m_socketError = false;
// Modified only in caller's thread.
CallerHandle::SignalType m_waitingFor = CallerHandle::SignalType::NoSignal;
}; };
class LauncherSocket : public QObject class LauncherSocket : public QObject
@@ -206,7 +246,7 @@ public:
void sendData(const QByteArray &data); void sendData(const QByteArray &data);
// Called from caller's thread exclusively. // Called from caller's thread exclusively.
LauncherHandle *registerHandle(quintptr token, ProcessMode mode); CallerHandle *registerHandle(quintptr token, ProcessMode mode);
void unregisterHandle(quintptr token); void unregisterHandle(quintptr token);
signals: signals:

View File

@@ -252,15 +252,15 @@ public:
: ProcessInterface(processMode), m_token(uniqueToken()) : ProcessInterface(processMode), m_token(uniqueToken())
{ {
m_handle = LauncherInterface::socket()->registerHandle(token(), processMode); m_handle = LauncherInterface::socket()->registerHandle(token(), processMode);
connect(m_handle, &LauncherHandle::errorOccurred, connect(m_handle, &CallerHandle::errorOccurred,
this, &ProcessInterface::errorOccurred); this, &ProcessInterface::errorOccurred);
connect(m_handle, &LauncherHandle::started, connect(m_handle, &CallerHandle::started,
this, &ProcessInterface::started); this, &ProcessInterface::started);
connect(m_handle, &LauncherHandle::finished, connect(m_handle, &CallerHandle::finished,
this, &ProcessInterface::finished); this, &ProcessInterface::finished);
connect(m_handle, &LauncherHandle::readyReadStandardOutput, connect(m_handle, &CallerHandle::readyReadStandardOutput,
this, &ProcessInterface::readyReadStandardOutput); this, &ProcessInterface::readyReadStandardOutput);
connect(m_handle, &LauncherHandle::readyReadStandardError, connect(m_handle, &CallerHandle::readyReadStandardError,
this, &ProcessInterface::readyReadStandardError); this, &ProcessInterface::readyReadStandardError);
} }
~ProcessLauncherImpl() override ~ProcessLauncherImpl() override
@@ -318,7 +318,7 @@ private:
const uint m_token = 0; const uint m_token = 0;
// Lives in launcher's thread. // Lives in launcher's thread.
LauncherHandle *m_handle = nullptr; CallerHandle *m_handle = nullptr;
}; };
void ProcessLauncherImpl::cancel() void ProcessLauncherImpl::cancel()
@@ -456,6 +456,9 @@ QtcProcess::QtcProcess(ProcessImpl processImpl, ProcessMode processMode, QObject
Q_UNUSED(qProcessProcessErrorMeta) Q_UNUSED(qProcessProcessErrorMeta)
} }
QtcProcess::QtcProcess(ProcessImpl processImpl, QObject *parent)
: QtcProcess(processImpl, ProcessMode::Reader, parent) {}
QtcProcess::QtcProcess(ProcessMode processMode, QObject *parent) QtcProcess::QtcProcess(ProcessMode processMode, QObject *parent)
: QtcProcess(defaultProcessImpl(), processMode, parent) {} : QtcProcess(defaultProcessImpl(), processMode, parent) {}

View File

@@ -66,6 +66,7 @@ public:
}; };
QtcProcess(ProcessImpl processImpl, ProcessMode processMode, QObject *parent = nullptr); QtcProcess(ProcessImpl processImpl, ProcessMode processMode, QObject *parent = nullptr);
QtcProcess(ProcessImpl processImpl, QObject *parent = nullptr);
QtcProcess(ProcessMode processMode, QObject *parent = nullptr); QtcProcess(ProcessMode processMode, QObject *parent = nullptr);
QtcProcess(QObject *parent = nullptr); QtcProcess(QObject *parent = nullptr);
~QtcProcess(); ~QtcProcess();