6#include <private/qfakexdgportal_p.h>
8#include <QtMultimedia/private/qmultimedia_ranges_p.h>
9#include <QtMultimedia/private/qpipewire_support_p.h>
10#include <QtCore/qcoreapplication.h>
11#include <QtCore/qdeadlinetimer.h>
12#include <QtCore/qdebug.h>
13#include <QtCore/qdir.h>
14#include <QtCore/qfile.h>
15#include <QtCore/qprocess.h>
16#include <QtCore/qstandardpaths.h>
17#include <QtCore/qthread.h>
18#include <QtCore/qvarlengtharray.h>
19#include <QtDBus/qdbusconnection.h>
20#include <QtDBus/qdbusinterface.h>
29# include <sys/prctl.h>
36using namespace Qt::StringLiterals;
37using namespace std::chrono_literals;
42constexpr const char *PrivateBusMarker =
"QT_MULTIMEDIA_TEST_XDG_PORTAL_PRIVATE_BUS";
43constexpr const char *FixtureModeVariable =
"QT_MULTIMEDIA_TEST_XDG_PORTAL_FIXTURE";
46constexpr const char *PrivateDataDirVariable =
"QT_MULTIMEDIA_TEST_XDG_PORTAL_DATA_DIR";
48constexpr auto StartupTimeout = 10s;
49constexpr int StartupTimeoutMs =
int(
std::chrono::milliseconds(StartupTimeout).count());
52
53
54
55
56
57
58
60
61
62
63
64
65
66
67constexpr const char *BusConfiguration = R"(<!DOCTYPE busconfig PUBLIC
68 "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
69 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
70<busconfig>
71 <type>session</type>
72 <keep_umask/>
73 <listen>unix:tmpdir=/tmp</listen>
74 <!-- deliberately no <servicedir>: nothing may be activated on this bus -->
75 <policy context="default">
76 <allow send_destination="*" eavesdrop="true"/>
77 <allow eavesdrop="true"/>
78 <allow own="*"/>
79 </policy>
80</busconfig>
81)";
83constexpr const char *DaemonConfiguration = R"(
84context.properties = {
85 link.max-buffers = 16
86 core.daemon = true
87 core.name = pipewire-0
88}
89
90context.spa-libs = {
91 support.* = support/libspa-support
92}
93
94context.modules = [
95 { name = libpipewire-module-rt
96 args = { nice.level = -11, rt.prio = 88 }
97 flags = [ ifexists nofail ]
98 }
99 { name = libpipewire-module-protocol-native }
100 { name = libpipewire-module-metadata }
101 { name = libpipewire-module-spa-node-factory }
102 { name = libpipewire-module-client-node }
103 { name = libpipewire-module-access }
104 { name = libpipewire-module-adapter }
105 { name = libpipewire-module-link-factory }
106]
107
108context.objects = []
109context.exec = []
110)";
112QString findPipeWireDaemon()
114 QString path = QStandardPaths::findExecutable(u"pipewire"_s);
116 path = QStandardPaths::findExecutable(u"pipewire"_s,
117 { u"/usr/bin"_s, u"/usr/local/bin"_s });
121void killChildWithParent()
125 ::prctl(PR_SET_PDEATHSIG, SIGKILL);
129bool waitForProcessOutput(QProcess &process, QByteArray *accumulated, QByteArrayView needle,
130 std::chrono::milliseconds timeout)
132 QDeadlineTimer deadline(timeout);
133 while (!deadline.hasExpired()) {
134 if (process.state() == QProcess::NotRunning && process.bytesAvailable() == 0)
136 if (process.waitForReadyRead(100))
137 accumulated->append(process.readAllStandardOutput());
138 if (accumulated->contains(needle))
160 if (!runtimeDir.isEmpty()) {
161 QDir(runtimeDir).removeRecursively();
167QFakeXdgPortalFixture::QFakeXdgPortalFixture() : d(std::make_unique<Private>()) { }
169QFakeXdgPortalFixture::~QFakeXdgPortalFixture()
174bool QFakeXdgPortalFixture::isHelperProcess(
int argc,
char **argv)
176 return std::any_of(argv + 1, argv + argc, [](
const char *arg) {
177 return std::string_view(arg) == QtFakeXdgPortal::HelperArgument;
181int QFakeXdgPortalFixture::runHelperProcess(
int argc,
char **argv)
183 return QtFakeXdgPortal::runPortalHelper(argc, argv);
186bool QFakeXdgPortalFixture::isRequested()
192 const QString mode = qEnvironmentVariable(FixtureModeVariable, u"auto"_s).toLower();
193 if (mode == u"none" || mode == u"0")
195 if (mode == u"fake" || mode == u"1")
200 return qEnvironmentVariable(
"XDG_SESSION_TYPE").toLower() != u"x11";
204bool QFakeXdgPortalFixture::prerequisitesAvailable(QString *unavailableReason)
206 const auto missing = [&](
const QString &reason) {
207 if (unavailableReason)
208 *unavailableReason = reason;
212 if (findPipeWireDaemon().isEmpty())
213 return missing(u"the 'pipewire' daemon was not found in PATH"_s);
214 if (QStandardPaths::findExecutable(u"dbus-run-session"_s).isEmpty())
215 return missing(u"'dbus-run-session' was not found in PATH"_s);
219bool QFakeXdgPortalFixture::isRunningOnPrivateBus()
221 return qEnvironmentVariableIsSet(PrivateBusMarker);
224void QFakeXdgPortalFixture::reexecUnderPrivateBusIfRequested(
int argc,
char **argv)
226 if (isRunningOnPrivateBus() || isHelperProcess(argc, argv) || !isRequested())
228 if (!prerequisitesAvailable(
nullptr))
231 const QString dbusRunSession = QStandardPaths::findExecutable(u"dbus-run-session"_s);
238 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
245 QByteArray dataDirTemplate =
"/tmp/qtportal.XXXXXX"_ba;
246 QString busConfigurationPath;
247 if (::mkdtemp(dataDirTemplate.data())) {
248 const QString dataDir = QString::fromLocal8Bit(dataDirTemplate);
249 environment.insert(u"XDG_DATA_DIRS"_s, dataDir);
250 environment.insert(u"XDG_DATA_HOME"_s, dataDir);
251 environment.insert(QString::fromLatin1(PrivateDataDirVariable), dataDir);
253 busConfigurationPath = dataDir + u"/session.conf"_s;
254 QFile busConfiguration(busConfigurationPath);
255 if (busConfiguration.open(QIODevice::WriteOnly))
256 busConfiguration.write(BusConfiguration);
258 busConfigurationPath.clear();
260 if (busConfigurationPath.isEmpty()) {
261 qWarning() <<
"QFakeXdgPortalFixture: could not disarm D-Bus activation; the real"
262 <<
"xdg-desktop-portal may claim the portal name first";
265 environment.insert(QString::fromLatin1(PrivateBusMarker), u"1"_s);
268 environment.insert(u"QT_SCREEN_CAPTURE_BACKEND"_s, u"pipewire"_s);
270 QVarLengthArray<
char *, 64> arguments;
271 QByteArray program = dbusRunSession.toLocal8Bit();
272 QByteArray configOption =
"--config-file="_ba + busConfigurationPath.toLocal8Bit();
273 QByteArray separator =
"--"_ba;
274 arguments.push_back(program.data());
275 if (!busConfigurationPath.isEmpty())
276 arguments.push_back(configOption.data());
277 arguments.push_back(separator.data());
278 std::copy(argv, argv + argc,
std::back_inserter(arguments));
279 arguments.push_back(
nullptr);
285 const QStringList environmentList = environment.toStringList();
286 std::vector<QByteArray> environmentStorage;
287 environmentStorage.reserve(environmentList.size());
288 QtMultimediaPrivate::ranges::transform(environmentList, std::back_inserter(environmentStorage),
289 [](
const QString &entry) {
290 return entry.toLocal8Bit();
295 QVarLengthArray<
char *, 64> envp;
296 std::transform(environmentStorage.begin(), environmentStorage.end(),
std::back_inserter(envp),
297 [](QByteArray &entry) {
300 envp.push_back(
nullptr);
302 ::execve(program.constData(), arguments.data(), envp.data());
307 qWarning() <<
"QFakeXdgPortalFixture: could not re-exec under dbus-run-session";
311
312
313
314
315
316q23::expected<
void, QString> QFakeXdgPortalFixture::startHelper(QSize frameSize)
321 d->helper.setProcessEnvironment(d->environment);
322 d->helper.setChildProcessModifier(killChildWithParent);
323 d->helper.setProgram(QCoreApplication::applicationFilePath());
324 d->helper.setArguments({
325 QString::fromLatin1(QtFakeXdgPortal::HelperArgument),
327 u"%1x%2"_s.arg(frameSize.width()).arg(frameSize.height()),
330 if (!d->helper.waitForStarted(StartupTimeoutMs))
331 return q23::unexpected(u"could not start the fake portal helper"_s);
333 if (!waitForProcessOutput(d->helper, &d->helperOutput,
334 QByteArrayView(
"QT_FAKE_XDG_PORTAL_READY"), StartupTimeout)) {
335 return q23::unexpected(u"the fake portal helper did not become ready: "_s
336 + QString::fromLocal8Bit(d->helper.readAllStandardError()));
339 d->nodeId = QtPipeWire::ObjectId{ d->helperOutput.split(
' ').value(1).trimmed().toUInt() };
340 d->frameSize = frameSize;
344void QFakeXdgPortalFixture::stopHelper()
346 if (d->helper.state() != QProcess::NotRunning) {
347 d->helper.terminate();
348 if (!d->helper.waitForFinished(2000))
350 d->helper.waitForFinished(2000);
352 d->helperOutput.clear();
353 d->nodeId = QtPipeWire::ObjectId{ 0 };
357q23::expected<
void, QString> QFakeXdgPortalFixture::start(
const SyntheticVideoScene &initialScene)
359 const auto fail = [
this](
const QString &reason) {
361 return q23::unexpected(reason);
365 return fail(u"already started"_s);
367 QString unavailableReason;
368 if (!prerequisitesAvailable(&unavailableReason))
369 return fail(unavailableReason);
371 if (!isRunningOnPrivateBus()) {
372 return fail(u"not running on a private session bus; "
373 "main() must call reexecUnderPrivateBusIfRequested()"_s);
378 QByteArray runtimeTemplate =
"/tmp/qtpw.XXXXXX"_ba;
379 if (!::mkdtemp(runtimeTemplate.data()))
380 return fail(u"could not create a private pipewire runtime directory"_s);
381 d->runtimeDir = QString::fromLocal8Bit(runtimeTemplate);
383 const QString configurationPath = d->runtimeDir + u"/qt-fake-xdg-portal.conf"_s;
384 QFile configuration(configurationPath);
385 if (!configuration.open(QIODevice::WriteOnly))
386 return fail(u"could not write "_s + configurationPath);
387 configuration.write(DaemonConfiguration);
388 configuration.close();
390 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
391 environment.insert(u"PIPEWIRE_RUNTIME_DIR"_s, d->runtimeDir);
392 environment.remove(u"PIPEWIRE_REMOTE"_s);
394 d->daemon.setProcessEnvironment(environment);
395 d->daemon.setChildProcessModifier(killChildWithParent);
396 d->daemon.setProgram(findPipeWireDaemon());
397 d->daemon.setArguments({ u"-c"_s, configurationPath });
399 if (!d->daemon.waitForStarted(StartupTimeoutMs))
400 return fail(u"could not start the private pipewire daemon"_s);
402 const QString socketPath = d->runtimeDir + u"/pipewire-0"_s;
403 QDeadlineTimer deadline(StartupTimeout);
404 while (!QFile::exists(socketPath)) {
405 if (deadline.hasExpired() || d->daemon.state() == QProcess::NotRunning) {
406 return fail(u"the private pipewire daemon did not create %1: %2"_s.arg(
407 socketPath, QString::fromLocal8Bit(d->daemon.readAllStandardError())));
412 d->environment = environment;
414 if (
auto result = startHelper(initialScene.frameSize); !result)
415 return fail(result.error());
419 return setScene(initialScene);
422void QFakeXdgPortalFixture::stop()
426 if (d->daemon.state() != QProcess::NotRunning) {
427 d->daemon.terminate();
428 if (!d->daemon.waitForFinished(2000))
430 d->daemon.waitForFinished(2000);
433 d->removeRuntimeDir();
434 if (qEnvironmentVariableIsSet(PrivateDataDirVariable)) {
435 QDir(qEnvironmentVariable(PrivateDataDirVariable)).removeRecursively();
436 qunsetenv(PrivateDataDirVariable);
438 d->helperOutput.clear();
439 d->nodeId = QtPipeWire::ObjectId{ 0 };
443bool QFakeXdgPortalFixture::isActive()
const
448quint32 QFakeXdgPortalFixture::nodeId()
const
450 return d->nodeId.value;
453q23::expected<
void, QString> QFakeXdgPortalFixture::setScene(
const SyntheticVideoScene &scene)
456 return q23::unexpected(u"the fixture is not running"_s);
458 if (scene.frameSize.isValid() && scene.frameSize != d->frameSize) {
459 if (
auto result = startHelper(scene.frameSize); !result)
463 QDBusInterface control(
464 u"org.freedesktop.portal.Desktop"_s, u"/org/qtproject/qtmultimedia/FakeXdgPortal"_s,
465 u"org.qtproject.qtmultimedia.FakeXdgPortal"_s, QDBusConnection::sessionBus());
466 const QDBusMessage reply = control.call(u"SetScene"_s, scene.toVariantMap());
467 if (reply.type() == QDBusMessage::ErrorMessage)
468 return q23::unexpected(u"SetScene failed: "_s + reply.errorMessage());
472q23::expected<
void, QString> QFakeXdgPortalFixture::closeSessions()
475 return q23::unexpected(u"the fixture is not running"_s);
477 QDBusInterface control(
478 u"org.freedesktop.portal.Desktop"_s, u"/org/qtproject/qtmultimedia/FakeXdgPortal"_s,
479 u"org.qtproject.qtmultimedia.FakeXdgPortal"_s, QDBusConnection::sessionBus());
480 const QDBusMessage reply = control.call(u"CloseSessions"_s);
481 if (reply.type() == QDBusMessage::ErrorMessage)
482 return q23::unexpected(u"CloseSessions failed: "_s + reply.errorMessage());
QtPipeWire::ObjectId nodeId
QProcessEnvironment environment