diff --git a/src/effects/screenshot/CMakeLists.txt b/src/effects/screenshot/CMakeLists.txt index 23d826dbfb..bbea5f7b9f 100644 --- a/src/effects/screenshot/CMakeLists.txt +++ b/src/effects/screenshot/CMakeLists.txt @@ -6,4 +6,7 @@ set(kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} ../service_utils.cpp screenshot/screenshot.cpp screenshot/screenshotdbusinterface1.cpp + screenshot/screenshotdbusinterface2.cpp ) + +qt5_add_dbus_adaptor(kwin4_effect_builtins_sources screenshot/org.kde.KWin.ScreenShot2.xml screenshot/screenshotdbusinterface2.h KWin::ScreenShotDBusInterface2) diff --git a/src/effects/screenshot/org.kde.KWin.ScreenShot2.xml b/src/effects/screenshot/org.kde.KWin.ScreenShot2.xml new file mode 100644 index 0000000000..34c03b2c1f --- /dev/null +++ b/src/effects/screenshot/org.kde.KWin.ScreenShot2.xml @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/effects/screenshot/screenshot.cpp b/src/effects/screenshot/screenshot.cpp index 1c715f8917..f292a8556d 100644 --- a/src/effects/screenshot/screenshot.cpp +++ b/src/effects/screenshot/screenshot.cpp @@ -10,6 +10,7 @@ */ #include "screenshot.h" #include "screenshotdbusinterface1.h" +#include "screenshotdbusinterface2.h" #include #include @@ -111,6 +112,7 @@ bool ScreenShotEffect::supported() ScreenShotEffect::ScreenShotEffect() : m_dbusInterface1(new ScreenShotDBusInterface1(this)) + , m_dbusInterface2(new ScreenShotDBusInterface2(this)) { connect(effects, &EffectsHandler::screenAdded, this, &ScreenShotEffect::handleScreenAdded); connect(effects, &EffectsHandler::screenRemoved, this, &ScreenShotEffect::handleScreenRemoved); diff --git a/src/effects/screenshot/screenshot.h b/src/effects/screenshot/screenshot.h index 454efc35de..7ab8169b24 100644 --- a/src/effects/screenshot/screenshot.h +++ b/src/effects/screenshot/screenshot.h @@ -31,6 +31,7 @@ enum ScreenShotFlag { Q_DECLARE_FLAGS(ScreenShotFlags, ScreenShotFlag) class ScreenShotDBusInterface1; +class ScreenShotDBusInterface2; struct ScreenShotWindowData; struct ScreenShotAreaData; struct ScreenShotScreenData; @@ -100,6 +101,7 @@ private: QVector m_screenScreenShots; QScopedPointer m_dbusInterface1; + QScopedPointer m_dbusInterface2; EffectScreen *m_paintedScreen = nullptr; }; diff --git a/src/effects/screenshot/screenshotdbusinterface1.h b/src/effects/screenshot/screenshotdbusinterface1.h index 77a8742c86..3cff6dbdb7 100644 --- a/src/effects/screenshot/screenshotdbusinterface1.h +++ b/src/effects/screenshot/screenshotdbusinterface1.h @@ -25,6 +25,8 @@ class ScreenShotSource1; * * An application that requests a screenshot must have "org.kde.kwin.Screenshot" listed in its * X-KDE-DBUS-Restricted-Interfaces desktop file field. + * + * @deprecated Use org.kde.KWin.ScreenShot2 interface instead. */ class ScreenShotDBusInterface1 : public QObject, protected QDBusContext { diff --git a/src/effects/screenshot/screenshotdbusinterface2.cpp b/src/effects/screenshot/screenshotdbusinterface2.cpp new file mode 100644 index 0000000000..56c35de8d4 --- /dev/null +++ b/src/effects/screenshot/screenshotdbusinterface2.cpp @@ -0,0 +1,477 @@ +/* + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Méven Car + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "screenshotdbusinterface2.h" +#include "../service_utils.h" +#include "screenshot2adaptor.h" + +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ + +static ScreenShotFlags screenShotFlagsFromOptions(const QVariantMap &options) +{ + ScreenShotFlags flags = ScreenShotFlags(); + + const QVariant includeDecoration = options.value(QStringLiteral("include-decoration")); + if (includeDecoration.toBool()) { + flags |= ScreenShotIncludeDecoration; + } + + const QVariant includeCursor = options.value(QStringLiteral("include-cursor")); + if (includeCursor.toBool()) { + flags |= ScreenShotIncludeCursor; + } + + const QVariant nativeResolution = options.value(QStringLiteral("native-resolution")); + if (nativeResolution.toBool()) { + flags |= ScreenShotNativeResolution; + } + + return flags; +} + +static void writeBufferToPipe(int fileDescriptor, const QByteArray &buffer) +{ + QFile file; + if (!file.open(fileDescriptor, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) { + close(fileDescriptor); + qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "failed to open pipe:" << file.errorString(); + return; + } + + qint64 remainingSize = buffer.size(); + + pollfd pfds[1]; + pfds[0].fd = fileDescriptor; + pfds[0].events = POLLOUT; + + while (true) { + const int ready = poll(pfds, 1, 60000); + if (ready < 0) { + if (errno != EINTR) { + qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "poll() failed:" << strerror(errno); + return; + } + } else if (ready == 0) { + qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "timed out writing to pipe"; + return; + } else if (!(pfds[0].revents & POLLOUT)) { + qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "pipe is broken"; + return; + } else { + const char *chunk = buffer.constData() + (buffer.size() - remainingSize); + const qint64 writtenCount = file.write(chunk, remainingSize); + + if (writtenCount < 0) { + qCWarning(KWINEFFECTS) << Q_FUNC_INFO << "write() failed:" << file.errorString(); + return; + } + + remainingSize -= writtenCount; + if (writtenCount == 0 || remainingSize == 0) { + return; + } + } + } +} + +static const QString s_dbusServiceName = QStringLiteral("org.kde.KWin.ScreenShot2"); +static const QString s_dbusInterface = QStringLiteral("org.kde.KWin.ScreenShot2"); +static const QString s_dbusObjectPath = QStringLiteral("/org/kde/KWin/ScreenShot2"); + +static const QString s_errorNotAuthorized = QStringLiteral("org.kde.KWin.ScreenShot2.Error.NoAuthorized"); +static const QString s_errorNotAuthorizedMessage = QStringLiteral("The process is not authorized to take a screenshot"); +static const QString s_errorCancelled = QStringLiteral("org.kde.KWin.ScreenShot2.Error.Cancelled"); +static const QString s_errorCancelledMessage = QStringLiteral("Screenshot got cancelled"); +static const QString s_errorInvalidWindow = QStringLiteral("org.kde.KWin.ScreenShot2.Error.InvalidWindow"); +static const QString s_errorInvalidWindowMessage = QStringLiteral("Invalid window requested"); +static const QString s_errorInvalidArea = QStringLiteral("org.kde.KWin.ScreenShot2.Error.InvalidArea"); +static const QString s_errorInvalidAreaMessage = QStringLiteral("Invalid area requested"); +static const QString s_errorInvalidScreen = QStringLiteral("org.kde.KWin.ScreenShot2.Error.InvalidScreen"); +static const QString s_errorInvalidScreenMessage = QStringLiteral("Invalid screen requested"); +static const QString s_errorFileDescriptor = QStringLiteral("org.kde.KWin.ScreenShot2.Error.FileDescriptor"); +static const QString s_errorFileDescriptorMessage = QStringLiteral("No valid file descriptor"); + +class ScreenShotSource2 : public QObject +{ + Q_OBJECT + +public: + explicit ScreenShotSource2(const QFuture &future); + + bool isCancelled() const; + bool isCompleted() const; + void marshal(ScreenShotSinkPipe2 *sink); + +Q_SIGNALS: + void cancelled(); + void completed(); + +private: + QFuture m_future; + QFutureWatcher *m_watcher; +}; + +class ScreenShotSourceScreen2 : public ScreenShotSource2 +{ + Q_OBJECT + +public: + ScreenShotSourceScreen2(ScreenShotEffect *effect, EffectScreen *screen, ScreenShotFlags flags); +}; + +class ScreenShotSourceArea2 : public ScreenShotSource2 +{ + Q_OBJECT + +public: + ScreenShotSourceArea2(ScreenShotEffect *effect, const QRect &area, ScreenShotFlags flags); +}; + +class ScreenShotSourceWindow2 : public ScreenShotSource2 +{ + Q_OBJECT + +public: + ScreenShotSourceWindow2(ScreenShotEffect *effect, EffectWindow *window, ScreenShotFlags flags); +}; + +class ScreenShotSinkPipe2 : public QObject +{ + Q_OBJECT + +public: + ScreenShotSinkPipe2(int fileDescriptor, QDBusMessage replyMessage); + ~ScreenShotSinkPipe2(); + + void cancel(); + void flush(const QImage &image); + +private: + QDBusMessage m_replyMessage; + int m_fileDescriptor; +}; + +ScreenShotSource2::ScreenShotSource2(const QFuture &future) + : m_future(future) +{ + m_watcher = new QFutureWatcher(this); + connect(m_watcher, &QFutureWatcher::finished, this, &ScreenShotSource2::completed); + connect(m_watcher, &QFutureWatcher::canceled, this, &ScreenShotSource2::cancelled); + m_watcher->setFuture(m_future); +} + +bool ScreenShotSource2::isCancelled() const +{ + return m_future.isCanceled(); +} + +bool ScreenShotSource2::isCompleted() const +{ + return m_future.isFinished(); +} + +void ScreenShotSource2::marshal(ScreenShotSinkPipe2 *sink) +{ + sink->flush(m_future.result()); +} + +ScreenShotSourceScreen2::ScreenShotSourceScreen2(ScreenShotEffect *effect, + EffectScreen *screen, + ScreenShotFlags flags) + : ScreenShotSource2(effect->scheduleScreenShot(screen, flags)) +{ +} + +ScreenShotSourceArea2::ScreenShotSourceArea2(ScreenShotEffect *effect, + const QRect &area, + ScreenShotFlags flags) + : ScreenShotSource2(effect->scheduleScreenShot(area, flags)) +{ +} + +ScreenShotSourceWindow2::ScreenShotSourceWindow2(ScreenShotEffect *effect, + EffectWindow *window, + ScreenShotFlags flags) + : ScreenShotSource2(effect->scheduleScreenShot(window, flags)) +{ +} + +ScreenShotSinkPipe2::ScreenShotSinkPipe2(int fileDescriptor, QDBusMessage replyMessage) + : m_replyMessage(replyMessage) + , m_fileDescriptor(fileDescriptor) +{ +} + +ScreenShotSinkPipe2::~ScreenShotSinkPipe2() +{ + if (m_fileDescriptor != -1) { + close(m_fileDescriptor); + } +} + +void ScreenShotSinkPipe2::cancel() +{ + QDBusConnection::sessionBus().send(m_replyMessage.createErrorReply(s_errorCancelled, + s_errorCancelledMessage)); +} + +void ScreenShotSinkPipe2::flush(const QImage &image) +{ + if (m_fileDescriptor == -1) { + return; + } + + // Note that the type of the data stored in the vardict matters. Be careful. + QVariantMap results; + results.insert(QStringLiteral("type"), QStringLiteral("raw")); + results.insert(QStringLiteral("format"), quint32(image.format())); + results.insert(QStringLiteral("width"), quint32(image.width())); + results.insert(QStringLiteral("height"), quint32(image.height())); + results.insert(QStringLiteral("stride"), quint32(image.bytesPerLine())); + QDBusConnection::sessionBus().send(m_replyMessage.createReply(results)); + + QtConcurrent::run([](int fileDescriptor, const QImage &image) { + const QByteArray buffer(reinterpret_cast(image.constBits()), + image.sizeInBytes()); + writeBufferToPipe(fileDescriptor, buffer); + }, m_fileDescriptor, image); + + // The ownership of the pipe file descriptor has been moved to the worker thread. + m_fileDescriptor = -1; +} + +ScreenShotDBusInterface2::ScreenShotDBusInterface2(ScreenShotEffect *effect) + : QObject(effect) + , m_effect(effect) +{ + new ScreenShot2Adaptor(this); + + QDBusConnection::sessionBus().registerObject(s_dbusObjectPath, this); + QDBusConnection::sessionBus().registerService(s_dbusServiceName); +} + +ScreenShotDBusInterface2::~ScreenShotDBusInterface2() +{ + QDBusConnection::sessionBus().unregisterService(s_dbusServiceName); + QDBusConnection::sessionBus().unregisterObject(s_dbusObjectPath); +} + +bool ScreenShotDBusInterface2::checkPermissions() const +{ + if (!calledFromDBus()) { + return false; + } + + const QDBusReply reply = connection().interface()->servicePid(message().service()); + if (reply.isValid()) { + const uint pid = reply.value(); + const auto interfaces = KWin::fetchRestrictedDBusInterfacesFromPid(pid); + if (!interfaces.contains(s_dbusInterface)) { + sendErrorReply(s_errorNotAuthorized, s_errorNotAuthorizedMessage); + return false; + } + } else { + return false; + } + + return true; +} + +QVariantMap ScreenShotDBusInterface2::CaptureWindow(const QString &handle, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + if (!checkPermissions()) { + return QVariantMap(); + } + + EffectWindow *window = effects->findWindow(handle); + if (!window) { + bool ok; + const int winId = handle.toInt(&ok); + if (ok) { + window = effects->findWindow(winId); + } else { + qCWarning(KWINEFFECTS) << "Invalid handle:" << handle; + } + } + if (!window) { + sendErrorReply(s_errorInvalidWindow, s_errorInvalidWindowMessage); + return QVariantMap(); + } + + const int fileDescriptor = dup(pipe.fileDescriptor()); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + takeScreenShot(window, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, message())); + + setDelayedReply(true); + return QVariantMap(); +} + +QVariantMap ScreenShotDBusInterface2::CaptureArea(int x, int y, int width, int height, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + if (!checkPermissions()) { + return QVariantMap(); + } + + const QRect area(x, y, width, height); + if (area.isEmpty()) { + sendErrorReply(s_errorInvalidArea, s_errorInvalidAreaMessage); + return QVariantMap(); + } + + const int fileDescriptor = dup(pipe.fileDescriptor()); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + takeScreenShot(area, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, message())); + + setDelayedReply(true); + return QVariantMap(); +} + +QVariantMap ScreenShotDBusInterface2::CaptureScreen(const QString &name, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + if (!checkPermissions()) { + return QVariantMap(); + } + + EffectScreen *screen = effects->findScreen(name); + if (!screen) { + sendErrorReply(s_errorInvalidScreen, s_errorInvalidScreenMessage); + return QVariantMap(); + } + + const int fileDescriptor = dup(pipe.fileDescriptor()); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + takeScreenShot(screen, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, message())); + + setDelayedReply(true); + return QVariantMap(); +} + +QVariantMap ScreenShotDBusInterface2::CaptureInteractive(uint kind, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + const int fileDescriptor = dup(pipe.fileDescriptor()); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + const QDBusMessage replyMessage = message(); + + if (kind == 0) { + effects->startInteractiveWindowSelection([=](EffectWindow *window) { + effects->hideOnScreenMessage(EffectsHandler::OnScreenMessageHideFlag::SkipsCloseAnimation); + + if (!window) { + close(fileDescriptor); + + QDBusConnection bus = QDBusConnection::sessionBus(); + bus.send(replyMessage.createErrorReply(s_errorCancelled, s_errorCancelledMessage)); + } else { + takeScreenShot(window, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, replyMessage)); + } + }); + effects->showOnScreenMessage(i18n("Select window to screen shot with left click or enter.\n" + "Escape or right click to cancel."), + QStringLiteral("spectacle")); + } else { + effects->startInteractivePositionSelection([=](const QPoint &point) { + effects->hideOnScreenMessage(EffectsHandler::OnScreenMessageHideFlag::SkipsCloseAnimation); + + if (point == QPoint(-1, -1)) { + close(fileDescriptor); + + QDBusConnection bus = QDBusConnection::sessionBus(); + bus.send(replyMessage.createErrorReply(s_errorCancelled, s_errorCancelledMessage)); + } else { + EffectScreen *screen = effects->screenAt(point); + takeScreenShot(screen, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, replyMessage)); + } + }); + effects->showOnScreenMessage(i18n("Create screen shot with left click or enter.\n" + "Escape or right click to cancel."), + QStringLiteral("spectacle")); + } + + setDelayedReply(true); + return QVariantMap(); +} + +void ScreenShotDBusInterface2::bind(ScreenShotSinkPipe2 *sink, ScreenShotSource2 *source) +{ + connect(source, &ScreenShotSource2::cancelled, sink, [sink, source]() { + sink->cancel(); + + sink->deleteLater(); + source->deleteLater(); + }); + + connect(source, &ScreenShotSource2::completed, sink, [sink, source]() { + source->marshal(sink); + + sink->deleteLater(); + source->deleteLater(); + }); +} + +void ScreenShotDBusInterface2::takeScreenShot(EffectScreen *screen, ScreenShotFlags flags, + ScreenShotSinkPipe2 *sink) +{ + bind(sink, new ScreenShotSourceScreen2(m_effect, screen, flags)); +} + +void ScreenShotDBusInterface2::takeScreenShot(const QRect &area, ScreenShotFlags flags, + ScreenShotSinkPipe2 *sink) +{ + bind(sink, new ScreenShotSourceArea2(m_effect, area, flags)); +} + +void ScreenShotDBusInterface2::takeScreenShot(EffectWindow *window, ScreenShotFlags flags, + ScreenShotSinkPipe2 *sink) +{ + bind(sink, new ScreenShotSourceWindow2(m_effect, window, flags)); +} + +} // namespace KWin + +#include "screenshotdbusinterface2.moc" diff --git a/src/effects/screenshot/screenshotdbusinterface2.h b/src/effects/screenshot/screenshotdbusinterface2.h new file mode 100644 index 0000000000..d44426fffd --- /dev/null +++ b/src/effects/screenshot/screenshotdbusinterface2.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "screenshot/screenshot.h" + +#include +#include +#include +#include + +namespace KWin +{ + +class ScreenShotEffect; +class ScreenShotSinkPipe2; +class ScreenShotSource2; + +/** + * The ScreenshotDBusInterface2 class provides a d-bus api to take screenshots. This implements + * the org.kde.KWin.ScreenShot2 interface. + * + * An application that requests a screenshot must have "org.kde.KWin.ScreenShot2" listed in its + * X-KDE-DBUS-Restricted-Interfaces desktop file field. + */ +class ScreenShotDBusInterface2 : public QObject, public QDBusContext +{ + Q_OBJECT + +public: + explicit ScreenShotDBusInterface2(ScreenShotEffect *effect); + ~ScreenShotDBusInterface2() override; + +public Q_SLOTS: + QVariantMap CaptureWindow(const QString &handle, const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + QVariantMap CaptureArea(int x, int y, int width, int height, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + QVariantMap CaptureScreen(const QString &name, const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + QVariantMap CaptureInteractive(uint kind, const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + +private: + void takeScreenShot(EffectScreen *screen, ScreenShotFlags flags, ScreenShotSinkPipe2 *sink); + void takeScreenShot(const QRect &area, ScreenShotFlags flags, ScreenShotSinkPipe2 *sink); + void takeScreenShot(EffectWindow *window, ScreenShotFlags flags, ScreenShotSinkPipe2 *sink); + + void bind(ScreenShotSinkPipe2 *sink, ScreenShotSource2 *source); + bool checkPermissions() const; + + ScreenShotEffect *m_effect; +}; + +} // namespace KWin