def _dbus_notify(title, message, duration=5000): from PyQt5.QtDBus import ( QDBus, QDBusArgument, QDBusConnection, QDBusInterface) bus = QDBusConnection.sessionBus() if not bus.isConnected(): raise OSError("Could not connect to DBus") interface = QDBusInterface( 'org.freedesktop.Notifications', '/org/freedesktop/Notifications', 'org.freedesktop.Notifications', bus) error = interface.lastError() if error.type(): raise RuntimeError("{}; {}".format(error.name(), error.message())) # See https://developer.gnome.org/notification-spec/ # "This allows clients to effectively modify the notification while # it's active. A value of value of 0 means that this notification # won't replace any existing notifications." replaces_id = QVariant(0) replaces_id.convert(QVariant.UInt) interface.call( QDBus.NoBlock, 'Notify', APP_NAME, replaces_id, resource(settings['application']['tray_icon']), title, message, QDBusArgument([], QMetaType.QStringList), {}, duration)
class YoudaoIndicator(QtCore.QObject): DBUS_NAME = "com.youdao.indicator" DBUS_PATH = "/com/youdao/indicator" DBUS_IFACE = "com.youdao.indicator" onMenuItemClicked = QtCore.pyqtSignal(str) onCheckMenuItemClicked = QtCore.pyqtSignal(str, bool) def __init__(self): QtCore.QObject.__init__(self) self.session_bus = QDBusConnection.sessionBus() self.session_bus.connect(self.DBUS_NAME, self.DBUS_PATH, self.DBUS_IFACE, 'MenuItemClicked', self.MenuItemClickedSlot) self.session_bus.connect(self.DBUS_NAME, self.DBUS_PATH, self.DBUS_IFACE, 'CheckMenuItemClicked', self.CheckMenuItemClickedSlot) self._iface = QDBusInterface(self.DBUS_NAME, self.DBUS_PATH, self.DBUS_IFACE, self.session_bus) @QtCore.pyqtSlot(result=bool) def isExists(self): self._iface.call("Hello") return not self._iface.lastError().isValid() @QtCore.pyqtSlot(str) def MenuItemClickedSlot(self, menu_id): self.onMenuItemClicked.emit(menu_id) @QtCore.pyqtSlot(str, bool) def CheckMenuItemClickedSlot(self, menu_id, enable): self.onCheckMenuItemClicked.emit(menu_id, enable) @QtCore.pyqtSlot(bool) def SetOcrEnable(self, enable): self._iface.call("SetOcrEnable", enable) @QtCore.pyqtSlot(bool) def SetStrokeEnable(self, enable): self._iface.call("SetStrokeEnable", enable) @QtCore.pyqtSlot() def Quit(self): self._iface.call("Quit")
class apiBase(): """Call the D-Bus camera APIs, asynchronously. Methods: - call(function[, arg1[ ,arg2[, ...]]]) Call the remote function. - get([value[, ...]]) Get the named values from the API. - set({key: value[, ...]}]) Set the named values in the API. All methods return an A* promise-like, in that you use `.then(cb(value))` and `.catch(cb(error))` to get the results of calling the function. """ def __init__(self, service, path, interface="", bus=QDBusConnection.systemBus()): if not QDBusConnection.systemBus().isConnected(): log.error("Can not connect to D-Bus. Is D-Bus itself running?") raise Exception("D-Bus Setup Error") self.name = type(self).__name__ self.iface = QDBusInterface(service, path, interface, bus) # For Asynchronous call handling. self.enqueuedCalls = [] self.callInProgress = False self.activeCall = None log.info("Connected to D-Bus %s API at %s", self.name, self.iface.path()) # Check for errors. if not self.iface.isValid(): # Otherwise, an error occured. log.error("Can not connect to %s D-Bus API at %s. (%s: %s)", self.name, self.iface.service(), self.iface.lastError().name(), self.iface.lastError().message()) else: self.iface.setTimeout(API_TIMEOUT_MS) def callSync(self, *args, warnWhenCallIsSlow=True, **kwargs): """Call a camera DBus API. First arg is the function name. This is the synchronous version of the call() method. It is much slower to call synchronously than asynchronously! See http://doc.qt.io/qt-5/qdbusabstractinterface.html#call for details about calling. See https://github.com/krontech/chronos-cli/tree/master/src/api for implementation details about the API being called. See README.md at https://github.com/krontech/chronos-cli/tree/master/src/daemon for API documentation. """ #Unwrap D-Bus errors from message. log.debug("%s.callSync %s", self.name, tuple(args)) start = perf_counter() msg = QDBusReply(self.iface.call(*args, **kwargs)) end = perf_counter() if warnWhenCallIsSlow and (end - start > API_SLOW_WARN_MS / 1000): log.warn(f'slow call: {self.name}.callSync{tuple(args)} took {(end-start)*1000:.0f}ms/{API_SLOW_WARN_MS}ms.') if msg.isValid(): return msg.value() else: if msg.error().name() == 'org.freedesktop.DBus.Error.NoReply': raise DBusException(f"{self.name}.callSync{tuple(args)} timed out ({API_TIMEOUT_MS}ms)") else: raise DBusException("%s: %s" % (msg.error().name(), msg.error().message())) def getSync(self, keyOrKeys): """Call a camera API DBus get method synchronously. Convenience method for `getSync('get', [value])[0]`. Accepts key or [key, …], where keys are strings. Returns value or {key:value, …}, respectively. See control's `availableKeys` for a list of valid inputs. """ valueList = self.callSync('get', [keyOrKeys] if isinstance(keyOrKeys, str) else keyOrKeys ) return valueList[keyOrKeys] if isinstance(keyOrKeys, str) else valueList def setSync(self, *args): """Call a camera API DBus set method synchronously. Accepts {str: value, ...} or a key and a value. Returns either a map of set values or the set value, if the second form was used. """ if len(args) == 1: return self.callSync('set', *args) elif len(args) == 2: return self.callSync('set', {args[0]:args[1]})[args[0]] else: raise valueError('bad args') def enqueueCall(self, pendingCall, coalesce: bool=True): #pendingCall is CallPromise """Enqueue callback. Squash and elide calls to set for efficiency.""" #Step 1: Will this call actually do anything? Elide it if not. anticipitoryUpdates = False #Emit update signals before sending the update to the API. Results in faster UI updates but poorer framerate. if coalesce and pendingCall._args[0] == 'set': #Elide this call if it would not change known state. hasNewInformation = False newItems = pendingCall._args[1].items() for key, value in newItems: if _camState[key] != value: hasNewInformation = True if not anticipitoryUpdates: break #Update known cam state in advance of state transition. log.info(f'Anticipating {key} → {value}.') _camState[key] = value for callback in apiValues._callbacks[key]: callback(value) if not hasNewInformation: return if coalesce and pendingCall._args[0] == 'playback': #Always merge playback states. #Take the playback state already enqueued, {}, and overlay the current playback state. (so, {a:1, b:1} + {b:2} = {a:1, b:2}) assert type(pendingCall._args[1]) is dict, f"playback() takes a {{key:value}} dict, got {pendingCall._args[1]} of type {type(pendingCall._args[1])}." existingParams = [call._args[1] for call in self.enqueuedCalls if call._args[0] == 'playback'] if not existingParams: self.enqueuedCalls += [pendingCall] else: #Update the parameters of the next playback call instead of enqueueing a new call. for k, v in pendingCall._args[1].items(): existingParams[-1][k] = v return #Step 2: Is there already a set call pending? (Note that non-set calls act as set barriers; two sets won't get coalesced if a non-set call is between them.) if coalesce and [pendingCall] == self.enqueuedCalls[:1]: self.enqueuedCalls[-1] = pendingCall else: self.enqueuedCalls += [pendingCall] def _startNextCallback(self): """Check for pending callbacks. If none are found, simply stop. Note: Needs to be manually pumped. """ if self.enqueuedCalls: self.callInProgress = True self.enqueuedCalls.pop(0)._startAsyncCall() else: self.callInProgress = False def call(self, *args): """Call a camera DBus API. First arg is the function name. Returns a promise. See http://doc.qt.io/qt-5/qdbusabstractinterface.html#call for details about calling. See https://github.com/krontech/chronos-cli/tree/master/src/api for implementation details about the API being called. See README.md at https://github.com/krontech/chronos-cli/tree/master/src/daemon for API documentation. """ promise = CallPromise(*args, api=self) log.debug(f'enquing {promise}') self.enqueueCall(promise) if not self.callInProgress: #Don't start multiple callbacks at once, the most recent one will block. self._startNextCallback() return promise def get(self, keyOrKeys): """Call a camera DBus API get method. Convenience method for `control('get', [value])[0]`. Accepts key or [key, …], where keys are strings. Returns value or {key:value, …}, respectively. See control's `availableKeys` for a list of valid inputs. """ return self.call( 'get', [keyOrKeys] if isinstance(keyOrKeys, str) else keyOrKeys ).then(lambda valueList: valueList[keyOrKeys] if isinstance(keyOrKeys, str) else valueList ) def set(self, *args): """Call a camera DBus API set method. Accepts {str: value, ...} or a key and a value. Returns either a map of set values or the set value, if the second form was used. """ log.debug(f'simple set call: {args}') if len(args) == 1: return self.call('set', *args) elif len(args) == 2: return self.call( 'set', {args[0]:args[1]} ).then(lambda valueDict: valueDict[args[0]] ) else: raise valueError('bad args')
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__(parent) assert _notifications_supported() 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( self._on_service_unregistered) test_service = 'test-notification-service' in objects.debug_flags service = f"{self.TEST_SERVICE}{os.getpid()}" 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, ver: 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(ver) <= 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") and ver != "devel": # 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(ver.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"): if utils.VersionNumber.parse(ver) < utils.VersionNumber(2, 0): # 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(ver) 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"): # Before refactor 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, ) elif (name, vendor) == ("Budgie Notification Server", "Budgie Desktop Developers"): # After refactor: https://github.com/BuddiesOfBudgie/budgie-desktop/pull/36 if utils.VersionNumber.parse(ver) < utils.VersionNumber(10, 6, 2): return _ServerQuirks( # https://github.com/BuddiesOfBudgie/budgie-desktop/issues/118 wrong_closes_type=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, ver, spec_version = reply.arguments() log.misc.debug( f"Connected to notification server: {name} {ver} by {vendor}, " f"implementing spec {spec_version}") quirks = self._find_quirks(name, vendor, ver) 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} {ver} 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: raise DBusError(msg) 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 _verify_notification_id( self, notification_id: int, *, replaces_id: int, ) -> None: """Ensure the returned notification id is valid.""" 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) if notification_id <= 0: self.error.emit(f"Got invalid notification id {notification_id}") def _get_title_arg(self, title: str) -> str: """Get the title argument for present().""" # Titles don't support markup (except with broken servers) if self._quirks.escape_title: return html.escape(title, quote=False) return title def _get_actions_arg(self) -> QDBusArgument: """Get the actions argument for present().""" actions = [] if self._capabilities.actions: actions = ['default', 'Activate'] # key, name return QDBusArgument(actions, QMetaType.QStringList) def _get_hints_arg(self, *, origin_url: QUrl, icon: QImage) -> Dict[str, Any]: """Get the hints argument for present().""" origin_url_str = origin_url.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(origin_url) if self._capabilities.kde_origin_name and is_useful_origin: hints["x-kde-origin-name"] = origin_url_str if icon.isNull(): filename = 'icons/qutebrowser-64x64.png' icon = QImage.fromData(resources.read_file_binary(filename)) key = self._quirks.icon_key or "image-data" data = self._convert_image(icon) if data is not None: hints[key] = data return hints def _call_notify_wrapper( self, *, appname: str, replaces_id: QVariant, icon: str, title: str, body: str, actions: QDBusArgument, hints: Dict[str, Any], timeout: int, ) -> Any: """Wrapper around DBus call to use keyword args.""" return self.interface.call( QDBus.BlockWithGui, "Notify", appname, replaces_id, icon, title, body, actions, hints, timeout, ) 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 reply = self._call_notify_wrapper( appname="qutebrowser", replaces_id=_as_uint32(replaces_id), icon="", # we use image-data and friends instead title=self._get_title_arg(qt_notification.title()), body=self._format_body( body=qt_notification.message(), origin_url=qt_notification.origin(), ), actions=self._get_actions_arg(), hints=self._get_hints_arg( origin_url=qt_notification.origin(), icon=qt_notification.icon(), ), timeout=-1, # use default ) try: self._verify_message(reply, "u", QDBusMessage.ReplyMessage) except DBusError as e: if e.is_fatal: raise self.error.emit(e.error_message) # Return value gets ignored in NotificationBridgePresenter.present return -1 notification_id = reply.arguments()[0] self._verify_notification_id(notification_id, replaces_id=replaces_id) 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, bytes_per_line, width, channel_count) 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.""" try: self._verify_message(msg, "uu", QDBusMessage.SignalMessage) except Error: if not self._quirks.wrong_closes_type: raise self._verify_message(msg, "ui", 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
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") class DBusException(Exception):