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))
161 if (!runtimeDir.isEmpty()) {
162 QDir(runtimeDir).removeRecursively();
168QFakeXdgPortalFixture::QFakeXdgPortalFixture() : d(std::make_unique<Private>()) { }
170QFakeXdgPortalFixture::~QFakeXdgPortalFixture()
175bool QFakeXdgPortalFixture::isHelperProcess(
int argc,
char **argv)
177 return std::any_of(argv + 1, argv + argc, [](
const char *arg) {
178 return std::string_view(arg) == QtFakeXdgPortal::HelperArgument;
182int QFakeXdgPortalFixture::runHelperProcess(
int argc,
char **argv)
184 return QtFakeXdgPortal::runPortalHelper(argc, argv);
187bool QFakeXdgPortalFixture::isForced()
192 const QString mode = qEnvironmentVariable(FixtureModeVariable).toLower();
193 return mode == u"fake" || mode == u"1";
197bool QFakeXdgPortalFixture::isRequested(QtFakeXdgPortal::Role role)
202 const QString mode = qEnvironmentVariable(FixtureModeVariable, u"auto"_s).toLower();
203 if (mode == u"none" || mode == u"0")
209 case QtFakeXdgPortal::Role::ScreenCast:
212 return qEnvironmentVariable(
"XDG_SESSION_TYPE").toLower() != u"x11";
213 case QtFakeXdgPortal::Role::Camera:
217 Q_UNREACHABLE_RETURN(
false);
221bool QFakeXdgPortalFixture::prerequisitesAvailable(QString *unavailableReason)
223 const auto missing = [&](
const QString &reason) {
224 if (unavailableReason)
225 *unavailableReason = reason;
229 if (findPipeWireDaemon().isEmpty())
230 return missing(u"the 'pipewire' daemon was not found in PATH"_s);
231 if (QStandardPaths::findExecutable(u"dbus-run-session"_s).isEmpty())
232 return missing(u"'dbus-run-session' was not found in PATH"_s);
236bool QFakeXdgPortalFixture::isRunningOnPrivateBus()
238 return qEnvironmentVariableIsSet(PrivateBusMarker);
241void QFakeXdgPortalFixture::reexecUnderPrivateBusIfRequested(
int argc,
char **argv,
242 QtFakeXdgPortal::Role role)
244 if (isRunningOnPrivateBus() || isHelperProcess(argc, argv) || !isRequested(role))
246 if (!prerequisitesAvailable(
nullptr))
249 const QString dbusRunSession = QStandardPaths::findExecutable(u"dbus-run-session"_s);
256 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
263 QByteArray dataDirTemplate =
"/tmp/qtportal.XXXXXX"_ba;
264 QString busConfigurationPath;
265 if (::mkdtemp(dataDirTemplate.data())) {
266 const QString dataDir = QString::fromLocal8Bit(dataDirTemplate);
267 environment.insert(u"XDG_DATA_DIRS"_s, dataDir);
268 environment.insert(u"XDG_DATA_HOME"_s, dataDir);
269 environment.insert(QString::fromLatin1(PrivateDataDirVariable), dataDir);
271 busConfigurationPath = dataDir + u"/session.conf"_s;
272 QFile busConfiguration(busConfigurationPath);
273 if (busConfiguration.open(QIODevice::WriteOnly))
274 busConfiguration.write(BusConfiguration);
276 busConfigurationPath.clear();
278 if (busConfigurationPath.isEmpty()) {
279 qWarning() <<
"QFakeXdgPortalFixture: could not disarm D-Bus activation; the real"
280 <<
"xdg-desktop-portal may claim the portal name first";
283 environment.insert(QString::fromLatin1(PrivateBusMarker), u"1"_s);
287 case QtFakeXdgPortal::Role::ScreenCast:
288 environment.insert(u"QT_SCREEN_CAPTURE_BACKEND"_s, u"pipewire"_s);
290 case QtFakeXdgPortal::Role::Camera:
291 environment.insert(u"QT_CAMERA_BACKEND"_s, u"pipewire"_s);
292 environment.insert(u"QT_PIPEWIRE_CAMERA_FORCE_PORTAL"_s, u"1"_s);
296 QVarLengthArray<
char *, 64> arguments;
297 QByteArray program = dbusRunSession.toLocal8Bit();
298 QByteArray configOption =
"--config-file="_ba + busConfigurationPath.toLocal8Bit();
299 QByteArray separator =
"--"_ba;
300 arguments.push_back(program.data());
301 if (!busConfigurationPath.isEmpty())
302 arguments.push_back(configOption.data());
303 arguments.push_back(separator.data());
304 std::copy(argv, argv + argc,
std::back_inserter(arguments));
305 arguments.push_back(
nullptr);
311 const QStringList environmentList = environment.toStringList();
312 std::vector<QByteArray> environmentStorage;
313 environmentStorage.reserve(environmentList.size());
314 QtMultimediaPrivate::ranges::transform(environmentList, std::back_inserter(environmentStorage),
315 [](
const QString &entry) {
316 return entry.toLocal8Bit();
321 QVarLengthArray<
char *, 64> envp;
322 std::transform(environmentStorage.begin(), environmentStorage.end(),
std::back_inserter(envp),
323 [](QByteArray &entry) {
326 envp.push_back(
nullptr);
328 ::execve(program.constData(), arguments.data(), envp.data());
333 qWarning() <<
"QFakeXdgPortalFixture: could not re-exec under dbus-run-session";
337
338
339
340
341
342q23::expected<
void, QString> QFakeXdgPortalFixture::startHelper(QSize frameSize)
347 d->helper.setProcessEnvironment(d->environment);
348 d->helper.setChildProcessModifier(killChildWithParent);
349 d->helper.setProgram(QCoreApplication::applicationFilePath());
350 d->helper.setArguments({
351 QString::fromLatin1(QtFakeXdgPortal::HelperArgument),
353 u"%1x%2"_s.arg(frameSize.width()).arg(frameSize.height()),
355 d->role == QtFakeXdgPortal::Role::Camera ? u"camera"_s : u"screencast"_s,
358 if (!d->helper.waitForStarted(StartupTimeoutMs))
359 return q23::unexpected(u"could not start the fake portal helper"_s);
361 if (!waitForProcessOutput(d->helper, &d->helperOutput,
362 QByteArrayView(
"QT_FAKE_XDG_PORTAL_READY"), StartupTimeout)) {
363 return q23::unexpected(u"the fake portal helper did not become ready: "_s
364 + QString::fromLocal8Bit(d->helper.readAllStandardError()));
367 d->nodeId = QtPipeWire::ObjectId{ d->helperOutput.split(
' ').value(1).trimmed().toUInt() };
368 d->frameSize = frameSize;
372void QFakeXdgPortalFixture::stopHelper()
374 if (d->helper.state() != QProcess::NotRunning) {
375 d->helper.terminate();
376 if (!d->helper.waitForFinished(2000))
378 d->helper.waitForFinished(2000);
380 d->helperOutput.clear();
381 d->nodeId = QtPipeWire::ObjectId{ 0 };
385q23::expected<
void, QString> QFakeXdgPortalFixture::start(
const SyntheticVideoScene &initialScene,
386 QtFakeXdgPortal::Role role)
388 const auto fail = [
this](
const QString &reason) {
390 return q23::unexpected(reason);
394 return fail(u"already started"_s);
396 QString unavailableReason;
397 if (!prerequisitesAvailable(&unavailableReason))
398 return fail(unavailableReason);
400 if (!isRunningOnPrivateBus()) {
401 return fail(u"not running on a private session bus; "
402 "main() must call reexecUnderPrivateBusIfRequested()"_s);
407 QByteArray runtimeTemplate =
"/tmp/qtpw.XXXXXX"_ba;
408 if (!::mkdtemp(runtimeTemplate.data()))
409 return fail(u"could not create a private pipewire runtime directory"_s);
410 d->runtimeDir = QString::fromLocal8Bit(runtimeTemplate);
412 const QString configurationPath = d->runtimeDir + u"/qt-fake-xdg-portal.conf"_s;
413 QFile configuration(configurationPath);
414 if (!configuration.open(QIODevice::WriteOnly))
415 return fail(u"could not write "_s + configurationPath);
416 configuration.write(DaemonConfiguration);
417 configuration.close();
419 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
420 environment.insert(u"PIPEWIRE_RUNTIME_DIR"_s, d->runtimeDir);
421 environment.remove(u"PIPEWIRE_REMOTE"_s);
423 d->daemon.setProcessEnvironment(environment);
424 d->daemon.setChildProcessModifier(killChildWithParent);
425 d->daemon.setProgram(findPipeWireDaemon());
426 d->daemon.setArguments({ u"-c"_s, configurationPath });
428 if (!d->daemon.waitForStarted(StartupTimeoutMs))
429 return fail(u"could not start the private pipewire daemon"_s);
431 const QString socketPath = d->runtimeDir + u"/pipewire-0"_s;
432 QDeadlineTimer deadline(StartupTimeout);
433 while (!QFile::exists(socketPath)) {
434 if (deadline.hasExpired() || d->daemon.state() == QProcess::NotRunning) {
435 return fail(u"the private pipewire daemon did not create %1: %2"_s.arg(
436 socketPath, QString::fromLocal8Bit(d->daemon.readAllStandardError())));
441 d->environment = environment;
444 if (
auto result = startHelper(initialScene.frameSize); !result)
445 return fail(result.error());
449 return setScene(initialScene);
452void QFakeXdgPortalFixture::stop()
456 if (d->daemon.state() != QProcess::NotRunning) {
457 d->daemon.terminate();
458 if (!d->daemon.waitForFinished(2000))
460 d->daemon.waitForFinished(2000);
463 d->removeRuntimeDir();
464 if (qEnvironmentVariableIsSet(PrivateDataDirVariable)) {
465 QDir(qEnvironmentVariable(PrivateDataDirVariable)).removeRecursively();
466 qunsetenv(PrivateDataDirVariable);
468 d->helperOutput.clear();
469 d->nodeId = QtPipeWire::ObjectId{ 0 };
473bool QFakeXdgPortalFixture::isActive()
const
478quint32 QFakeXdgPortalFixture::nodeId()
const
480 return d->nodeId.value;
483q23::expected<
void, QString> QFakeXdgPortalFixture::setScene(
const SyntheticVideoScene &scene)
486 return q23::unexpected(u"the fixture is not running"_s);
488 if (scene.frameSize.isValid() && scene.frameSize != d->frameSize) {
489 if (
auto result = startHelper(scene.frameSize); !result)
493 QDBusInterface control(
494 u"org.freedesktop.portal.Desktop"_s, u"/org/qtproject/qtmultimedia/FakeXdgPortal"_s,
495 u"org.qtproject.qtmultimedia.FakeXdgPortal"_s, QDBusConnection::sessionBus());
496 const QDBusMessage reply = control.call(u"SetScene"_s, scene.toVariantMap());
497 if (reply.type() == QDBusMessage::ErrorMessage)
498 return q23::unexpected(u"SetScene failed: "_s + reply.errorMessage());
502q23::expected<
void, QString> QFakeXdgPortalFixture::closeSessions()
505 return q23::unexpected(u"the fixture is not running"_s);
507 QDBusInterface control(
508 u"org.freedesktop.portal.Desktop"_s, u"/org/qtproject/qtmultimedia/FakeXdgPortal"_s,
509 u"org.qtproject.qtmultimedia.FakeXdgPortal"_s, QDBusConnection::sessionBus());
510 const QDBusMessage reply = control.call(u"CloseSessions"_s);
511 if (reply.type() == QDBusMessage::ErrorMessage)
512 return q23::unexpected(u"CloseSessions failed: "_s + reply.errorMessage());
QtPipeWire::ObjectId nodeId
QProcessEnvironment environment
QtFakeXdgPortal::Role role