def __init__(self, dbus_name, object_name):
     QObject.__init__(self)
     
     iface = QDBusInterface(dbus_name, object_name, '', QDBusConnection.sessionBus())
     if iface.isValid():
         iface.call("unique")
         sys.exit(1)
     
     QDBusConnection.sessionBus().registerService(dbus_name)
     QDBusConnection.sessionBus().registerObject(object_name, self, QDBusConnection.ExportAllSlots)
Esempio n. 2
0
    def __init__(self, dbus_name, object_name):
        QObject.__init__(self)

        search_option = len(sys.argv) >= 2 and sys.argv[1] == "--search"
        iface = QDBusInterface(dbus_name, object_name, '', QDBusConnection.sessionBus())
        
        if iface.isValid():
            iface.call("unique")

            if search_option:
                iface.call("search")
            
            sys.exit(1)
        
        QDBusConnection.sessionBus().registerService(dbus_name)
        QDBusConnection.sessionBus().registerObject(object_name, self, QDBusConnection.ExportAllSlots)
Esempio n. 3
0
cameraControlAPI = QDBusInterface(
	f"ca.krontech.chronos.{'control_mock' if USE_MOCK else 'control'}", #Service
	f"/ca/krontech/chronos/{'control_mock' if USE_MOCK else 'control'}", #Path
	f"", #Interface
	QDBusConnection.systemBus() )
cameraVideoAPI = QDBusInterface(
	f"ca.krontech.chronos.{'video_mock' if USE_MOCK else 'video'}", #Service
	f"/ca/krontech/chronos/{'video_mock' if USE_MOCK else 'video'}", #Path
	f"", #Interface
	QDBusConnection.systemBus() )

cameraControlAPI.setTimeout(API_TIMEOUT_MS) #Default is -1, which means 25000ms. 25 seconds is too long to go without some sort of feedback, and the only real long-running operation we have - saving - can take upwards of 5 minutes. Instead of setting the timeout to half an hour, we use events which are emitted as the task progresses. One frame (at 15fps) should be plenty of time for the API to respond, and also quick enough that we'll notice any slowness.
cameraVideoAPI.setTimeout(API_TIMEOUT_MS)

if not cameraControlAPI.isValid():
	print("Error: Can not connect to control D-Bus API at %s. (%s: %s)" % (
		cameraControlAPI.service(), 
		cameraControlAPI.lastError().name(), 
		cameraControlAPI.lastError().message(),
	), file=sys.stderr)
	raise Exception("D-Bus Setup Error")

if not cameraVideoAPI.isValid():
	print("Error: Can not connect to video D-Bus API at %s. (%s: %s)" % (
		cameraVideoAPI.service(), 
		cameraVideoAPI.lastError().name(), 
		cameraVideoAPI.lastError().message(),
	), file=sys.stderr)
	raise Exception("D-Bus Setup Error")
Esempio n. 4
0
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtDBus import QDBusConnection, QDBusInterface, QDBusReply

if __name__ == '__main__':
    app = QCoreApplication(sys.argv)

    if not QDBusConnection.sessionBus().isConnected():
        sys.stderr.write("Cannot connect to the D-Bus session bus.\n"
                         "To start it, run:\n"
                         "\teval `dbus-launch --auto-syntax`\n")
        sys.exit(1)

    iface = QDBusInterface('org.example.QtDBus.PingExample', '/', '',
                           QDBusConnection.sessionBus())

    if iface.isValid():
        msg = iface.call('ping', sys.argv[1] if len(sys.argv) > 1 else "")
        reply = QDBusReply(msg)

        if reply.isValid():
            sys.stdout.write("Reply was: %s\n" % reply.value())
            sys.exit()

        sys.stderr.write("Call failed: %s\n" % reply.error().message())
        sys.exit(1)

    sys.stderr.write("%s\n" %
                     QDBusConnection.sessionBus().lastError().message())
    sys.exit(1)
Esempio n. 5
0
class DBusNotificationAdapter(AbstractNotificationAdapter):

    """Send notifications over DBus.

    This is essentially what libnotify does, except using Qt's DBus implementation.

    Related specs:
    https://developer.gnome.org/notification-spec/
    https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html
    https://wiki.ubuntu.com/NotificationDevelopmentGuidelines
    """

    SERVICE = "org.freedesktop.Notifications"
    TEST_SERVICE = "org.qutebrowser.TestNotifications"
    PATH = "/org/freedesktop/Notifications"
    INTERFACE = "org.freedesktop.Notifications"
    SPEC_VERSION = "1.2"  # Released in January 2011, still current in March 2021.
    NAME = "libnotify"

    def __init__(self, parent: QObject = None) -> None:
        super().__init__(bridge)
        if not qtutils.version_check('5.14'):
            raise Error("Notifications are not supported on Qt < 5.14")

        if utils.is_windows:
            # The QDBusConnection destructor seems to cause error messages (and
            # potentially segfaults) on Windows, so we bail out early in that case.
            # We still try to get a connection on macOS, since it's theoretically
            # possible to run DBus there.
            raise Error("libnotify is not supported on Windows")

        bus = QDBusConnection.sessionBus()
        if not bus.isConnected():
            raise Error(
                "Failed to connect to DBus session bus: " +
                self._dbus_error_str(bus.lastError()))

        self._watcher = QDBusServiceWatcher(
            self.SERVICE,
            bus,
            QDBusServiceWatcher.WatchForUnregistration,
            self,
        )
        self._watcher.serviceUnregistered.connect(  # type: ignore[attr-defined]
            self._on_service_unregistered)

        test_service = 'test-notification-service' in objects.debug_flags
        service = self.TEST_SERVICE if test_service else self.SERVICE

        self.interface = QDBusInterface(service, self.PATH, self.INTERFACE, bus)
        if not self.interface.isValid():
            raise Error(
                "Could not construct a DBus interface: " +
                self._dbus_error_str(self.interface.lastError()))

        connections = [
            ("NotificationClosed", self._handle_close),
            ("ActionInvoked", self._handle_action),
        ]
        for name, func in connections:
            if not bus.connect(service, self.PATH, self.INTERFACE, name, func):
                raise Error(
                    f"Could not connect to {name}: " +
                    self._dbus_error_str(bus.lastError()))

        self._quirks = _ServerQuirks()
        if not test_service:
            # Can't figure out how to make this work with the test server...
            # https://www.riverbankcomputing.com/pipermail/pyqt/2021-March/043724.html
            self._get_server_info()

        if self._quirks.skip_capabilities:
            self._capabilities = _ServerCapabilities.from_list([])
        else:
            self._fetch_capabilities()

    @pyqtSlot(str)
    def _on_service_unregistered(self) -> None:
        """Make sure we know when the notification daemon exits.

        If that's the case, we bail out, as otherwise notifications would fail or the
        next start of the server would lead to duplicate notification IDs.
        """
        log.misc.debug("Notification daemon did quit!")
        self.clear_all.emit()

    def _find_quirks(  # noqa: C901 ("too complex"
        self,
        name: str,
        vendor: str,
        version: str,
    ) -> Optional[_ServerQuirks]:
        """Find quirks to use based on the server information."""
        if (name, vendor) == ("notify-osd", "Canonical Ltd"):
            # Shows a dialog box instead of a notification bubble as soon as a
            # notification has an action (even if only a default one). Dialog boxes are
            # buggy and return a notification with ID 0.
            # https://wiki.ubuntu.com/NotificationDevelopmentGuidelines#Avoiding_actions
            return _ServerQuirks(avoid_actions=True, spec_version="1.1")
        elif (name, vendor) == ("Notification Daemon", "MATE"):
            # Still in active development but doesn't implement spec 1.2:
            # https://github.com/mate-desktop/mate-notification-daemon/issues/132
            quirks = _ServerQuirks(spec_version="1.1")
            if utils.VersionNumber.parse(version) <= utils.VersionNumber(1, 24):
                # https://github.com/mate-desktop/mate-notification-daemon/issues/118
                quirks.avoid_body_hyperlinks = True
            return quirks
        elif (name, vendor) == ("naughty", "awesome"):
            # Still in active development but spec 1.0/1.2 support isn't
            # released yet:
            # https://github.com/awesomeWM/awesome/commit/e076bc664e0764a3d3a0164dabd9b58d334355f4
            parsed_version = utils.VersionNumber.parse(version.lstrip('v'))
            if parsed_version <= utils.VersionNumber(4, 3):
                return _ServerQuirks(spec_version="1.0")
        elif (name, vendor) == ("twmnd", "twmnd"):
            # https://github.com/sboli/twmn/pull/96
            return _ServerQuirks(spec_version="0")
        elif (name, vendor) == ("tiramisu", "Sweets"):
            # https://github.com/Sweets/tiramisu/issues/20
            return _ServerQuirks(skip_capabilities=True)
        elif (name, vendor) == ("lxqt-notificationd", "lxqt.org"):
            quirks = _ServerQuirks()
            parsed_version = utils.VersionNumber.parse(version)
            if parsed_version <= utils.VersionNumber(0, 16):
                # https://github.com/lxqt/lxqt-notificationd/issues/253
                quirks.escape_title = True
            if parsed_version < utils.VersionNumber(0, 16):
                # https://github.com/lxqt/lxqt-notificationd/commit/c23e254a63c39837fb69d5c59c5e2bc91e83df8c
                quirks.icon_key = 'image_data'
            return quirks
        elif (name, vendor) == ("haskell-notification-daemon", "abc"):  # aka "deadd"
            return _ServerQuirks(
                # https://github.com/phuhl/linux_notification_center/issues/160
                spec_version="1.0",
                # https://github.com/phuhl/linux_notification_center/issues/161
                wrong_replaces_id=True,
            )
        elif (name, vendor) == ("ninomiya", "deifactor"):
            return _ServerQuirks(
                no_padded_images=True,
                wrong_replaces_id=True,
            )
        elif (name, vendor) == ("Raven", "Budgie Desktop Developers"):
            return _ServerQuirks(
                # https://github.com/solus-project/budgie-desktop/issues/2114
                escape_title=True,
                # https://github.com/solus-project/budgie-desktop/issues/2115
                wrong_replaces_id=True,
            )
        return None

    def _get_server_info(self) -> None:
        """Query notification server information and set quirks."""
        reply = self.interface.call(QDBus.BlockWithGui, "GetServerInformation")
        self._verify_message(reply, "ssss", QDBusMessage.ReplyMessage)
        name, vendor, version, spec_version = reply.arguments()

        log.misc.debug(
            f"Connected to notification server: {name} {version} by {vendor}, "
            f"implementing spec {spec_version}")

        quirks = self._find_quirks(name, vendor, version)
        if quirks is not None:
            log.misc.debug(f"Enabling quirks {quirks}")
            self._quirks = quirks

        expected_spec_version = self._quirks.spec_version or self.SPEC_VERSION
        if spec_version != expected_spec_version:
            log.misc.warning(
                f"Notification server ({name} {version} by {vendor}) implements "
                f"spec {spec_version}, but {expected_spec_version} was expected. "
                f"If {name} is up to date, please report a qutebrowser bug.")

        # https://specifications.freedesktop.org/notification-spec/latest/ar01s08.html
        icon_key_overrides = {
            "1.0": "icon_data",
            "1.1": "image_data",
        }
        if spec_version in icon_key_overrides:
            self._quirks.icon_key = icon_key_overrides[spec_version]

    def _dbus_error_str(self, error: QDBusError) -> str:
        """Get a string for a DBus error."""
        if not error.isValid():
            return "Unknown error"
        return f"{error.name()} - {error.message()}"

    def _verify_message(
        self,
        msg: QDBusMessage,
        expected_signature: str,
        expected_type: QDBusMessage.MessageType,
    ) -> None:
        """Check the signature/type of a received message.

        Raises DBusError if the signature doesn't match.
        """
        assert expected_type not in [
            QDBusMessage.ErrorMessage,
            QDBusMessage.InvalidMessage,
        ], expected_type

        if msg.type() == QDBusMessage.ErrorMessage:
            err = msg.errorName()
            if err == "org.freedesktop.DBus.Error.NoReply":
                self.error.emit(msg.errorMessage())  # notification daemon is gone
                return

            raise Error(f"Got DBus error: {err} - {msg.errorMessage()}")

        signature = msg.signature()
        if signature != expected_signature:
            raise Error(
                f"Got a message with signature {signature} but expected "
                f"{expected_signature} (args: {msg.arguments()})")

        typ = msg.type()
        if typ != expected_type:
            type_str = debug.qenum_key(QDBusMessage.MessageType, typ)
            expected_type_str = debug.qenum_key(QDBusMessage.MessageType, expected_type)
            raise Error(
                f"Got a message of type {type_str} but expected {expected_type_str}"
                f"(args: {msg.arguments()})")

    def present(
        self,
        qt_notification: "QWebEngineNotification",
        *,
        replaces_id: Optional[int],
    ) -> int:
        """Shows a notification over DBus."""
        if replaces_id is None:
            replaces_id = 0  # 0 is never a valid ID according to the spec

        actions = []
        if self._capabilities.actions:
            actions = ['default', 'Activate']  # key, name
        actions_arg = QDBusArgument(actions, QMetaType.QStringList)

        origin_url_str = qt_notification.origin().toDisplayString()
        hints: Dict[str, Any] = {
            # Include the origin in case the user wants to do different things
            # with different origin's notifications.
            "x-qutebrowser-origin": origin_url_str,
            "desktop-entry": "org.qutebrowser.qutebrowser",
        }

        is_useful_origin = self._should_include_origin(qt_notification.origin())
        if self._capabilities.kde_origin_name and is_useful_origin:
            hints["x-kde-origin-name"] = origin_url_str

        icon = qt_notification.icon()
        if icon.isNull():
            filename = ':/icons/qutebrowser-64x64.png'
            icon = QImage(filename)

        key = self._quirks.icon_key or "image-data"
        data = self._convert_image(icon)
        if data is not None:
            hints[key] = data

        # Titles don't support markup (except with broken servers)
        title = qt_notification.title()
        if self._quirks.escape_title:
            title = html.escape(title, quote=False)

        reply = self.interface.call(
            QDBus.BlockWithGui,
            "Notify",
            "qutebrowser",  # application name
            _as_uint32(replaces_id),  # replaces notification id
            "",  # icon name/file URL, we use image-data and friends instead.
            title,
            self._format_body(qt_notification.message(), qt_notification.origin()),
            actions_arg,
            hints,
            -1,  # timeout; -1 means 'use default'
        )
        self._verify_message(reply, "u", QDBusMessage.ReplyMessage)

        notification_id = reply.arguments()[0]

        if replaces_id not in [0, notification_id]:
            msg = (
                f"Wanted to replace notification {replaces_id} but got new id "
                f"{notification_id}."
            )
            if self._quirks.wrong_replaces_id:
                log.misc.debug(msg)
            else:
                log.misc.error(msg)

        return notification_id

    def _convert_image(self, qimage: QImage) -> Optional[QDBusArgument]:
        """Convert a QImage to the structure DBus expects.

        https://specifications.freedesktop.org/notification-spec/latest/ar01s05.html#icons-and-images-formats
        """
        bits_per_color = 8
        has_alpha = qimage.hasAlphaChannel()
        if has_alpha:
            image_format = QImage.Format_RGBA8888
            channel_count = 4
        else:
            image_format = QImage.Format_RGB888
            channel_count = 3

        qimage.convertTo(image_format)
        bytes_per_line = qimage.bytesPerLine()
        width = qimage.width()
        height = qimage.height()

        image_data = QDBusArgument()
        image_data.beginStructure()
        image_data.add(width)
        image_data.add(height)
        image_data.add(bytes_per_line)
        image_data.add(has_alpha)
        image_data.add(bits_per_color)
        image_data.add(channel_count)

        try:
            size = qimage.sizeInBytes()
        except TypeError:
            # WORKAROUND for
            # https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042919.html
            # byteCount() is obsolete, but sizeInBytes() is only available with
            # SIP >= 5.3.0.
            size = qimage.byteCount()

        # Despite the spec not mandating this, many notification daemons mandate that
        # the last scanline does not have any padding bytes.
        #
        # Or in the words of dunst:
        #
        #     The image is serialised rowwise pixel by pixel. The rows are aligned by a
        #     spacer full of garbage. The overall data length of data + garbage is
        #     called the rowstride.
        #
        #     Mind the missing spacer at the last row.
        #
        #     len:     |<--------------rowstride---------------->|
        #     len:     |<-width*pixelstride->|
        #     row 1:   |   data for row 1    | spacer of garbage |
        #     row 2:   |   data for row 2    | spacer of garbage |
        #              |         .           | spacer of garbage |
        #              |         .           | spacer of garbage |
        #              |         .           | spacer of garbage |
        #     row n-1: |   data for row n-1  | spacer of garbage |
        #     row n:   |   data for row n    |
        #
        # Source:
        # https://github.com/dunst-project/dunst/blob/v1.6.1/src/icon.c#L292-L309
        padding = bytes_per_line - width * channel_count
        assert 0 <= padding < 3, padding
        size -= padding

        if padding and self._quirks.no_padded_images:
            return None

        bits = qimage.constBits().asstring(size)
        image_data.add(QByteArray(bits))

        image_data.endStructure()
        return image_data

    @pyqtSlot(QDBusMessage)
    def _handle_close(self, msg: QDBusMessage) -> None:
        """Handle NotificationClosed from DBus."""
        self._verify_message(msg, "uu", QDBusMessage.SignalMessage)
        notification_id, _close_reason = msg.arguments()
        self.close_id.emit(notification_id)

    @pyqtSlot(QDBusMessage)
    def _handle_action(self, msg: QDBusMessage) -> None:
        """Handle ActionInvoked from DBus."""
        self._verify_message(msg, "us", QDBusMessage.SignalMessage)
        notification_id, action_key = msg.arguments()
        if action_key == "default":
            self.click_id.emit(notification_id)

    @pyqtSlot(int)
    def on_web_closed(self, notification_id: int) -> None:
        """Send CloseNotification if a notification was closed from JS."""
        self.interface.call(
            QDBus.NoBlock,
            "CloseNotification",
            _as_uint32(notification_id),
        )

    def _fetch_capabilities(self) -> None:
        """Fetch capabilities from the notification server."""
        reply = self.interface.call(
            QDBus.BlockWithGui,
            "GetCapabilities",
        )
        self._verify_message(reply, "as", QDBusMessage.ReplyMessage)

        caplist = reply.arguments()[0]
        self._capabilities = _ServerCapabilities.from_list(caplist)
        if self._quirks.avoid_actions:
            self._capabilities.actions = False
        if self._quirks.avoid_body_hyperlinks:
            self._capabilities.body_hyperlinks = False

        log.misc.debug(f"Notification server capabilities: {self._capabilities}")

    def _format_body(self, body: str, origin_url: QUrl) -> str:
        """Format the body according to the server capabilities.

        If the server doesn't support x-kde-origin-name, we include the origin URL as a
        prefix. If possible, we hyperlink it.

        For both prefix and body, we'll need to HTML escape it if the server supports
        body markup.
        """
        urlstr = origin_url.toDisplayString()
        is_useful_origin = self._should_include_origin(origin_url)

        if self._capabilities.kde_origin_name or not is_useful_origin:
            prefix = None
        elif self._capabilities.body_markup and self._capabilities.body_hyperlinks:
            href = html.escape(
                origin_url.toString(QUrl.FullyEncoded)  # type: ignore[arg-type]
            )
            text = html.escape(urlstr, quote=False)
            prefix = f'<a href="{href}">{text}</a>'
        elif self._capabilities.body_markup:
            prefix = html.escape(urlstr, quote=False)
        else:
            prefix = urlstr

        if self._capabilities.body_markup:
            body = html.escape(body, quote=False)

        if prefix is None:
            return body

        return prefix + '\n\n' + body
Esempio n. 6
0
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtDBus import QDBusConnection, QDBusInterface, QDBusReply


if __name__ == '__main__':
    app = QCoreApplication(sys.argv)

    if not QDBusConnection.sessionBus().isConnected():
        sys.stderr.write("Cannot connect to the D-Bus session bus.\n"
                "To start it, run:\n"
                "\teval `dbus-launch --auto-syntax`\n");
        sys.exit(1)

    iface = QDBusInterface('org.example.QtDBus.PingExample', '/', '',
            QDBusConnection.sessionBus())

    if iface.isValid():
        msg = iface.call('ping', sys.argv[1] if len(sys.argv) > 1 else "")
        reply = QDBusReply(msg)

        if reply.isValid():
            sys.stdout.write("Reply was: %s\n" % reply.value())
            sys.exit()

        sys.stderr.write("Call failed: %s\n" % reply.error().message())
        sys.exit(1)

    sys.stderr.write("%s\n" % QDBusConnection.sessionBus().lastError().message())
    sys.exit(1)