QProcess/Unix: add a few, basic session & terminal management flags

Doing setsid() and disconnecting from the controlling terminal are, in
addition to resetting the standard file descriptors to /dev/null, a
common task that daemons do. These options allow a QProcess to force a
child to be a daemon.

QProcess ensures that the operations are done in the correct order.

Change-Id: I3e3bfef633af4130a03afffd175e9451d2716d7a
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Thiago Macieira 2023-05-12 19:50:51 -07:00
parent 8f1df9aaa8
commit 13a1995e9d
5 changed files with 113 additions and 2 deletions

View File

@ -849,6 +849,22 @@ void QProcessPrivate::Channel::clear()
child. The \c stdin, \c stdout, and \c stderr file descriptors are
never closed.
\value [since 6.7] CreateNewSession Starts a new process session, by calling
\c{setsid(2)}. This allows the child process to outlive the session
the current process is in. This is one of the steps that
startDetached() takes to allow the process to detach, and is also one
of the steps to daemonize a process.
\value [since 6.7] DisconnectControllingTerminal Requests that the process
disconnect from its controlling terminal, if it has one. If it has
none, nothing happens. Processes still connected to a controlling
terminal may get a Hang Up (\c SIGHUP) signal if the terminal
closes, or one of the other terminal-control signals (\c SIGTSTP, \c
SIGTTIN, \c SIGTTOU). Note that on some operating systems, a process
may only disconnect from the controlling terminal if it is the
session leader, meaning the \c CreateNewSession flag may be
required. Like it, this is one of the steps to daemonize a process.
\value IgnoreSigPipe Always sets the \c SIGPIPE signal to ignored
(\c SIG_IGN), even if the \c ResetSignalHandlers flag was set. By
default, if the child attempts to write to its standard output or

View File

@ -182,6 +182,8 @@ public:
// some room if we want to add IgnoreSigHup or so
CloseFileDescriptors = 0x0010,
UseVFork = 0x0020, // like POSIX_SPAWN_USEVFORK
CreateNewSession = 0x0040, // like POSIX_SPAWN_SETSID
DisconnectControllingTerminal = 0x0080,
};
Q_DECLARE_FLAGS(UnixProcessFlags, UnixProcessFlag)
struct UnixProcessParameters

View File

@ -39,6 +39,9 @@
#include <sys/resource.h>
#include <unistd.h>
#if __has_include(<paths.h>)
# include <paths.h>
#endif
#if __has_include(<linux/close_range.h>)
// FreeBSD's is in <unistd.h>
# include <linux/close_range.h>
@ -51,6 +54,12 @@
#ifndef O_PATH
# define O_PATH 0
#endif
#ifndef _PATH_DEV
# define _PATH_DEV "/dev/"
#endif
#ifndef _PATH_TTY
# define _PATH_TTY _PATH_DEV "tty"
#endif
#ifdef Q_OS_FREEBSD
__attribute__((weak))
@ -774,7 +783,7 @@ void QProcess::failChildProcessModifier(const char *description, int error) noex
}
// See IMPORTANT notice below
static void applyProcessParameters(const QProcess::UnixProcessParameters &params)
static const char *applyProcessParameters(const QProcess::UnixProcessParameters &params)
{
// Apply Unix signal handler parameters.
// We don't expect signal() to fail, so we ignore its return value
@ -817,6 +826,29 @@ static void applyProcessParameters(const QProcess::UnixProcessParameters &params
close(fd);
}
}
// Apply session and process group settings. This may fail.
if (params.flags.testFlag(QProcess::UnixProcessFlag::CreateNewSession)) {
if (setsid() < 0)
return "setsid";
}
// Disconnect from the controlling TTY. This probably won't fail. Must be
// done after the session settings from above.
if (params.flags.testFlag(QProcess::UnixProcessFlag::DisconnectControllingTerminal)) {
if (int fd = open(_PATH_TTY, O_RDONLY | O_NOCTTY); fd >= 0) {
// we still have a controlling TTY; give it up
int r = ioctl(fd, TIOCNOTTY);
int savedErrno = errno;
close(fd);
if (r != 0) {
errno = savedErrno;
return "ioctl";
}
}
}
return nullptr;
}
// the noexcept here adds an extra layer of protection
@ -857,7 +889,8 @@ void QChildProcess::startProcess() const noexcept
callChildProcessModifier(d);
// then we apply our other user-provided parameters
applyProcessParameters(d->unixExtras->processParameters);
if (const char *what = applyProcessParameters(d->unixExtras->processParameters))
failChildProcess(d, what, errno);
auto flags = d->unixExtras->processParameters.flags;
using P = QProcess::UnixProcessFlag;

View File

@ -80,6 +80,22 @@ int main(int argc, char **argv)
return EXIT_SUCCESS;
}
if (cmd == "noctty") {
int fd = open("/dev/tty", O_RDONLY);
if (fd == -1)
return EXIT_SUCCESS;
fprintf(stderr, "Could open /dev/tty\n");
return EXIT_FAILURE;
}
if (cmd == "setsid") {
pid_t pgid = getpgrp();
if (pgid == getpid())
return EXIT_SUCCESS;
fprintf(stderr, "Process group was %d\n", pgid);
return EXIT_FAILURE;
}
fprintf(stderr, "Unknown command \"%s\"", cmd.data());
return EXIT_FAILURE;
}

View File

@ -126,6 +126,8 @@ private slots:
void raiseInChildProcessModifier();
void unixProcessParameters_data();
void unixProcessParameters();
void impossibleUnixProcessParameters_data();
void impossibleUnixProcessParameters();
void unixProcessParametersAndChildModifier();
void unixProcessParametersOtherFileDescriptors();
#endif
@ -1730,6 +1732,10 @@ void tst_QProcess::unixProcessParameters_data()
addRow("reset-sighand", P::ResetSignalHandlers);
addRow("ignore-sigpipe", P::IgnoreSigPipe);
addRow("file-descriptors", P::CloseFileDescriptors);
addRow("setsid", P::CreateNewSession);
// On FreeBSD, we need to be session leader to disconnect from the CTTY
addRow("noctty", P::DisconnectControllingTerminal | P::CreateNewSession);
}
void tst_QProcess::unixProcessParameters()
@ -1778,6 +1784,13 @@ void tst_QProcess::unixProcessParameters()
}
} scope;
if (params.flags & QProcess::UnixProcessFlag::DisconnectControllingTerminal) {
if (int fd = open("/dev/tty", O_RDONLY); fd < 0) {
qInfo("Process has no controlling terminal; this test will do nothing");
close(fd);
}
}
QProcess process;
process.setUnixProcessParameters(params);
process.setStandardInputFile(QProcess::nullDevice()); // so we can't mess with SIGPIPE
@ -1798,6 +1811,31 @@ void tst_QProcess::unixProcessParameters()
QCOMPARE(process.exitStatus(), QProcess::NormalExit);
}
void tst_QProcess::impossibleUnixProcessParameters_data()
{
using P = QProcess::UnixProcessParameters;
QTest::addColumn<P>("params");
QTest::newRow("setsid") << P{ QProcess::UnixProcessFlag::CreateNewSession };
}
void tst_QProcess::impossibleUnixProcessParameters()
{
QFETCH(QProcess::UnixProcessParameters, params);
QProcess process;
if (params.flags & QProcess::UnixProcessFlag::CreateNewSession) {
process.setChildProcessModifier([]() {
// double setsid() should cause the second to fail
setsid();
});
}
process.setUnixProcessParameters(params);
process.start("testProcessNormal/testProcessNormal");
QVERIFY(!process.waitForStarted(5000));
qDebug() << process.errorString();
}
void tst_QProcess::unixProcessParametersAndChildModifier()
{
static constexpr char message[] = "Message from the handler function\n";
@ -1806,6 +1844,8 @@ void tst_QProcess::unixProcessParametersAndChildModifier()
QAtomicInt vforkControl;
int pipes[2];
pid_t oldpgid = getpgrp();
QVERIFY2(pipe(pipes) == 0, qPrintable(qt_error_string()));
auto pipeGuard0 = qScopeGuard([=] { close(pipes[0]); });
{
@ -1813,10 +1853,14 @@ void tst_QProcess::unixProcessParametersAndChildModifier()
// verify that our modifier runs before the parameters are applied
process.setChildProcessModifier([=, &vforkControl] {
const char *pgidmsg = "PGID mismatch. ";
if (getpgrp() != oldpgid)
write(pipes[1], pgidmsg, strlen(pgidmsg));
write(pipes[1], message, strlen(message));
vforkControl.storeRelaxed(1);
});
auto flags = QProcess::UnixProcessFlag::CloseFileDescriptors |
QProcess::UnixProcessFlag::CreateNewSession |
QProcess::UnixProcessFlag::UseVFork;
process.setUnixProcessParameters({ flags });
process.setProgram("testUnixProcessParameters/testUnixProcessParameters");