QProcess/Unix: introduce setChildProcessModifier()

[ChangeLog][Source-Incompatible Changes] QProcess::setupChildProcess()
was removed. To execute code in a child process, use
QProcess::setChildProcessModifier()

[ChangeLog][QtCore][QProcess] Added setChildProcessModifier() function
with which one can provide code to be run in the Unix child process
between fork() and execve(). With this function, it is no longer
necessary to derive from QProcess in order to execute actions in the
child process.

Another reason is that we can tell whether the std::function carries a
valid target much more easily than we can tell whether QProcess was
overridden.

The setupChildProcess() virtual function does not need to be marked
final, since no overrider could ever return an inaccessible private
class. This also makes sure the error presented to the user is about the
return type, not about attempting to override a final.

Clang:
 error: virtual function 'f' has a different return type ('void') than the function it overrides (which has return type 'QProcess::Use_setChildProcessModifier_Instead')

GCC:
 error: conflicting return type specified for 'virtual void MyProcess::setupChildProcess()'
 note: overridden function is 'virtual QProcess::Use_setChildProcessModifier_Instead QProcess::setupChildProcess()'

ICC:
 error: return type is neither identical to nor covariant with return type "QProcess::Use_setChildProcessModifier_Instead" of overridden virtual function "QProcess::setupChildProcess"

MSVC is not relevant since it doesn't compile to Unix.

Change-Id: Ia8b65350cd5d49debca9fffd15f801161363aea7
Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@gmx.de>
This commit is contained in:
Thiago Macieira 2020-02-29 14:47:43 -08:00
parent 3deb154d22
commit 7e93870401
7 changed files with 128 additions and 85 deletions

View File

@ -90,28 +90,22 @@ process2.start("command2");
//! [4]
class SandboxProcess : public QProcess
void runSandboxed(const QString &name, const QStringList &arguments)
{
...
protected:
void setupChildProcess() override;
...
};
void SandboxProcess::setupChildProcess()
{
// Drop all privileges in the child process, and enter
// a chroot jail.
#if defined Q_OS_UNIX
::setgroups(0, 0);
::chroot("/etc/safe");
::chdir("/");
::setgid(safeGid);
::setuid(safeUid);
::umask(0);
#endif
QProcess proc;
proc.setChildProcessModifier([] {
// Drop all privileges in the child process, and enter
// a chroot jail.
::setgroups(0, 0);
::chroot("/etc/safe");
::chdir("/");
::setgid(safeGid);
::setuid(safeUid);
::umask(0);
});
proc.start(name, arguments);
proc.waitForFinished();
}
//! [4]

View File

@ -1529,7 +1529,7 @@ QProcess::CreateProcessArgumentModifier QProcess::createProcessArgumentsModifier
\note This function is available only on the Windows platform and requires
C++11.
\sa QProcess::CreateProcessArgumentModifier
\sa QProcess::CreateProcessArgumentModifier, setChildProcessModifier()
*/
void QProcess::setCreateProcessArgumentsModifier(CreateProcessArgumentModifier modifier)
{
@ -1539,6 +1539,59 @@ void QProcess::setCreateProcessArgumentsModifier(CreateProcessArgumentModifier m
#endif
#if defined(Q_OS_UNIX) || defined(Q_QDOC)
/*!
\since 6.0
Returns the modifier function previously set by calling
setChildProcessModifier().
\note This function is only available on Unix platforms.
\sa setChildProcessModifier()
*/
std::function<void(void)> QProcess::childProcessModifier() const
{
Q_D(const QProcess);
return d->childProcessModifier;
}
/*!
\since 6.0
Sets the \a modifier function for the child process, for Unix systems
(including \macos; for Windows, see setCreateProcessArgumentsModifier()).
The function contained by the \a modifier argument will be invoked in the
child process after \c{fork()} is completed and QProcess has set up the
standard file descriptors for the child process, but before \c{execve()},
inside start(). The modifier is useful to change certain properties of the
child process, such as setting up additional file descriptors or closing
others, changing the nice level, disconnecting from the controlling TTY,
etc.
The following shows an example of setting up a child process to run without
privileges:
\snippet code/src_corelib_io_qprocess.cpp 4
If the modifier function needs to exit the process, remember to use
\c{_exit()}, not \c{exit()}.
\note In multithreaded applications, this function must be careful not to
call any functions that may lock mutexes that may have been in use in
other threads (in general, using only functions defined by POSIX as
"async-signal-safe" is advised). Most of the Qt API is unsafe inside this
callback, including qDebug(), and may lead to deadlocks.
\sa childProcessModifier()
*/
void QProcess::setChildProcessModifier(const std::function<void(void)> &modifier)
{
Q_D(QProcess);
d->childProcessModifier = modifier;
}
#endif
/*!
If QProcess has been assigned a working directory, this function returns
the working directory that the QProcess will enter before the program has
@ -1843,25 +1896,16 @@ void QProcess::setProcessState(ProcessState state)
emit stateChanged(state, QPrivateSignal());
}
#if QT_VERSION < QT_VERSION_CHECK(7,0,0)
/*!
This function is called in the child process context just before the
program is executed on Unix or \macos (i.e., after \c fork(), but before
\c execve()). Reimplement this function to do last minute initialization
of the child process. Example:
\snippet code/src_corelib_io_qprocess.cpp 4
You cannot exit the process (by calling exit(), for instance) from
this function. If you need to stop the program before it starts
execution, your workaround is to emit finished() and then call
exit().
\warning This function is called by QProcess on Unix and \macos
only. On Windows and QNX, it is not called.
\internal
*/
void QProcess::setupChildProcess()
auto QProcess::setupChildProcess() -> Use_setChildProcessModifier_Instead
{
Q_UNREACHABLE();
return {};
}
#endif
/*! \reimp
*/

View File

@ -205,6 +205,10 @@ public:
CreateProcessArgumentModifier createProcessArgumentsModifier() const;
void setCreateProcessArgumentsModifier(CreateProcessArgumentModifier modifier);
#endif // Q_OS_WIN || Q_CLANG_QDOC
#if defined(Q_OS_UNIX) || defined(Q_CLANG_QDOC)
std::function<void(void)> childProcessModifier() const;
void setChildProcessModifier(const std::function<void(void)> &modifier);
#endif
QString workingDirectory() const;
void setWorkingDirectory(const QString &dir);
@ -263,8 +267,6 @@ Q_SIGNALS:
protected:
void setProcessState(ProcessState state);
virtual void setupChildProcess();
// QIODevice
qint64 readData(char *data, qint64 maxlen) override;
qint64 writeData(const char *data, qint64 len) override;
@ -273,6 +275,15 @@ private:
Q_DECLARE_PRIVATE(QProcess)
Q_DISABLE_COPY(QProcess)
#if QT_VERSION < QT_VERSION_CHECK(7,0,0)
// ### Qt7: Remove this struct and the virtual function; they're here only
// to cause build errors in Qt 5 code that wasn't updated to Qt 6's
// setChildProcessModifier()
struct Use_setChildProcessModifier_Instead {};
QT_DEPRECATED_X("Use setChildProcessModifier() instead")
virtual Use_setChildProcessModifier_Instead setupChildProcess();
#endif
Q_PRIVATE_SLOT(d_func(), bool _q_canReadStandardOutput())
Q_PRIVATE_SLOT(d_func(), bool _q_canReadStandardError())
Q_PRIVATE_SLOT(d_func(), bool _q_canWrite())

View File

@ -322,6 +322,8 @@ public:
#if defined(Q_OS_WIN)
QString nativeArguments;
QProcess::CreateProcessArgumentModifier modifyCreateProcessArgs;
#else
std::function<void(void)> childProcessModifier;
#endif
QProcessEnvironment environment;

View File

@ -459,7 +459,7 @@ void QProcessPrivate::startProcess()
// safe with vfork semantics: suspend the parent execution until the child
// either execve()s or _exit()s.
int ffdflags = FFD_CLOEXEC;
if (typeid(*q) != typeid(QProcess))
if (childProcessModifier)
ffdflags |= FFD_USE_FORK;
pid_t childPid;
forkfd = ::forkfd(ffdflags , &childPid);
@ -544,7 +544,6 @@ void QProcessPrivate::execChild(const char *workingDir, char **argv, char **envp
{
::signal(SIGPIPE, SIG_DFL); // reset the signal that we ignored
Q_Q(QProcess);
ChildError error = { 0, {} }; // force zeroing of function[8]
// copy the stdin socket if asked to (without closing on exec)
@ -574,8 +573,8 @@ void QProcessPrivate::execChild(const char *workingDir, char **argv, char **envp
goto report_errno;
}
// this is a virtual call, and it base behavior is to do nothing.
q->setupChildProcess();
if (childProcessModifier)
childProcessModifier();
// execute the process
if (!envp) {

View File

@ -63,7 +63,7 @@ private slots:
void getSetCheck();
void constructing();
void simpleStart();
void setupChildProcess();
void setChildProcessModifier();
void startWithOpen();
void startWithOldOpen();
void execute();
@ -267,49 +267,40 @@ void tst_QProcess::simpleStart()
QCOMPARE(qvariant_cast<QProcess::ProcessState>(spy.at(2).at(0)), QProcess::NotRunning);
}
void tst_QProcess::setupChildProcess()
static const char messageFromChildProcess[] = "Message from the child process";
static void childProcessModifier(int fd)
{
/* This test exists because in Qt 5.15, the Unix version of QProcess has
* some code that depends on whether it's an actual QProcess or a
* derived class */
static const char setupChildMessage[] = "Called from setupChildProcess()";
class DerivedProcessClass : public QProcess {
public:
int fd;
DerivedProcessClass(int fd) : fd(fd)
{
}
QT_WRITE(fd, messageFromChildProcess, sizeof(messageFromChildProcess) - 1);
QT_CLOSE(fd);
}
protected:
void setupChildProcess() override
{
QT_WRITE(fd, setupChildMessage, sizeof(setupChildMessage) - 1);
QT_CLOSE(fd);
}
};
int pipes[2] = { -1 , -1 };
void tst_QProcess::setChildProcessModifier()
{
#ifdef Q_OS_UNIX
int pipes[2] = { -1 , -1 };
QVERIFY(qt_safe_pipe(pipes) == 0);
#endif
DerivedProcessClass process(pipes[1]);
QProcess process;
process.setChildProcessModifier([pipes]() {
::childProcessModifier(pipes[1]);
});
process.start("testProcessNormal/testProcessNormal");
if (process.state() != QProcess::Starting)
QCOMPARE(process.state(), QProcess::Running);
QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString()));
#ifdef Q_OS_UNIX
char buf[sizeof setupChildMessage] = {};
char buf[sizeof messageFromChildProcess] = {};
qt_safe_close(pipes[1]);
QCOMPARE(qt_safe_read(pipes[0], buf, sizeof(buf)), qint64(sizeof(setupChildMessage) - 1));
QCOMPARE(buf, setupChildMessage);
QCOMPARE(qt_safe_read(pipes[0], buf, sizeof(buf)), qint64(sizeof(messageFromChildProcess)) - 1);
QCOMPARE(buf, messageFromChildProcess);
qt_safe_close(pipes[0]);
#endif
QVERIFY2(process.waitForFinished(5000), qPrintable(process.errorString()));
QCOMPARE(process.exitStatus(), QProcess::NormalExit);
QCOMPARE(process.exitCode(), 0);
#else
QSKIP("Unix-only test");
#endif
}
void tst_QProcess::startWithOpen()

View File

@ -86,26 +86,28 @@ namespace QTest {
}
}
# ifdef Q_OS_UNIX
class QExternalProcess: public QProcess
{
protected:
#ifdef Q_OS_UNIX
void setupChildProcess()
public:
QExternalProcess()
{
// run in user code
QProcess::setupChildProcess();
if (processChannelMode() == ForwardedChannels) {
// reopen /dev/tty into stdin
int fd = ::open("/dev/tty", O_RDONLY);
if (fd == -1)
return;
::dup2(fd, 0);
::close(fd);
}
setChildProcessModifier([this]() {
// run in user code
if (processChannelMode() == ForwardedChannels) {
// reopen /dev/tty into stdin
int fd = ::open("/dev/tty", O_RDONLY);
if (fd == -1)
return;
::dup2(fd, 0);
::close(fd);
}
});
}
#endif
};
# else
using QExternalProcess = QProcess;
# endif
#endif // QT_CONFIG(process)
class QExternalTestPrivate