コード例 #1
0
class SignalTraceLoaderDBus(SignalTraceLoader):
    DBUS_SERVICE_NAME = 'cz.cuni.natur.echmet.edii'
    DBUS_OBJECT_PATH = '/EDII'

    ABI_VERSION_MAJOR = 0
    ABI_VERSION_MINOR = 1

    def _fetchABIVersion(self):
        msg = self._dbusIface.call('abiVersion')
        if msg.type() != QDBusMessage.ReplyMessage:
            raise DBusReplyError('Invalid reply to abiVersion request')

        if len(msg.arguments()) != 1:
            raise DBusReplyError('Invalid reply length to abiVersion request')

        return msg.arguments()[0]

    def _fetchSupportedFormats(self):
        msg = self._dbusIface.call('supportedFileFormats')
        if msg.type() != QDBusMessage.ReplyMessage:
            raise DBusReplyError('Invalid reply to supportedFileFormats request')

        if len(msg.arguments()) < 1:
            raise DBusReplyError('Invalid reply length to supportedFileFormats request')

        sffs = []
        for sff in msg.arguments()[0]:
            loadingOptions = []
            for idx in range(0, len(sff[3])):
                opt = sff[3][idx]
                loadingOptions.append(LoadingOption(idx, opt))

            sffs.append(SupportedFileFormat(sff[2], sff[1], sff[0], loadingOptions))

        return sffs

    @staticmethod
    def _payloadToSignal(block):
        path = block[0]
        dataID = block[1]
        xTitle = block[3]
        xUnit = block[5]
        yTitle = block[4]
        yUnit = block[6]
        datapoints = []

        for pt in block[7]:
            datapoints.append(Datapoint(x=pt[0], y=pt[1]))

        return SignalTrace(datapoints, path, dataID, xTitle, xUnit, yTitle, yUnit)

    def loadSignal(self, tag, option, hintpath):
        if not SignalTraceLoaderDBus.serviceAvailable():
            try:
                self._loaderLauncher.launchIfNeeded()
            except EDIIConnectionError:
                return []
            except EDIIStartError:
                return []

        msg = None
        if len(hintpath) > 0:
            msg = self._dbusIface.call('loadDataHint', tag, hintpath, option)
        else:
            msg = self._dbusIface.call('loadData', tag, option)
        if msg.type() != QDBusMessage.ReplyMessage:
            raise DBusReplyError('Invalid reply to loadData request')

        if len(msg.arguments()) < 1:
            raise DBusReplyError('Invalid reply length to loadData request')

        payload = msg.arguments()[0]

        if payload[0] is False:
            raise NoDataError()

        signals = []
        for block in payload[2]:
            signals.append(self._payloadToSignal(block))

        return signals

    def __init__(self, loaderLauncher):
        super().__init__(loaderLauncher)

        if not QDBusConnection.sessionBus().interface().isServiceRegistered(SignalTraceLoaderDBus.DBUS_SERVICE_NAME):
            raise DBusInterfaceError('Service {} is not registered'.format(SignalTraceLoaderDBus.DBUS_SERVICE_NAME))

        self._dbusIface = QDBusInterface(SignalTraceLoaderDBus.DBUS_SERVICE_NAME, SignalTraceLoaderDBus.DBUS_OBJECT_PATH, '')
        if not self._dbusIface.isValid():
            raise DBusInterfaceError('DBus interface is invalid')

        abiVersion = self._fetchABIVersion()

        if abiVersion[0] != self.ABI_VERSION_MAJOR or abiVersion[1] != self.ABI_VERSION_MINOR:
            raise DBusInterfaceError('Incompatible version od DBus interface ABI {}.{}'.format(abiVersion[0], abiVersion[1]))

        self._supportedFileFormats = self._fetchSupportedFormats()
        self._dbusIface.setTimeout(600000)

    def serviceAvailable():
        if not QDBusConnection.sessionBus().interface().isServiceRegistered(SignalTraceLoaderDBus.DBUS_SERVICE_NAME):
            return False

        dbusIface = QDBusInterface(SignalTraceLoaderDBus.DBUS_SERVICE_NAME, SignalTraceLoaderDBus.DBUS_OBJECT_PATH, '')
        return dbusIface.isValid()

    def supportedFileFormats(self):
        return self._supportedFileFormats
コード例 #2
0
class NetworkInterfaces(QObject):
	def __init__(self):
		"""
			NetworkInterfaces is a list of the plugged-in network connections.
			
			```python
				[{
					"path": b"/org/freedesktop/NetworkManager/Devices/1"
					"name": "Ethernet",
					"address": "192.168.100.166" or "2001:0db8:85a3::8a2e:0370:7334"
				}, {
					...
				}]
			```
			
			You can `networkInterfaces.observe(callback)` to get updates.
			
		"""
		super().__init__()
		self._connections = []
		
		#observers collection
		self._callbacks = []
		self.networkManager = QDBusInterface(
			f"org.freedesktop.NetworkManager", #Service
			f"/org/freedesktop/NetworkManager", #Path
			f"org.freedesktop.NetworkManager", #Interface
			QDBusConnection.systemBus(),
		)
		self.networkManager.setTimeout(10) #Set to 1000 after startup period.
		
		#Retry. This doesn't connect the first time, no matter what the time limit is. I don't know why, probably something in the start-on-demand logic.
		if not self.networkManager.isValid():
			self.networkManager = QDBusInterface(
				f"org.freedesktop.NetworkManager", #Service
				f"/org/freedesktop/NetworkManager", #Path
				f"org.freedesktop.NetworkManager", #Interface
				QDBusConnection.systemBus(),
			)
			self.networkManager.setTimeout(10)
			
			if not self.networkManager.isValid():
				log.critical(f"Error: Can not connect to NetworkManager at {self.networkManager.service()}. ({self.networkManager.lastError().name()}: {self.networkManager.lastError().message()}) Try running `apt install network-manager`?")
				raise Exception("D-Bus Setup Error")
		
		self.networkManager.setTimeout(1000)
		
		
		#The .connect call freezes if we don't do this, or if we do this twice.
		#This bug was fixed by Qt 5.11.
		QDBusConnection.systemBus().registerObject(
			f"/org/freedesktop/NetworkManager", 
			self,
		)
		
		self._acquireInterfacesCall = QDBusPendingCallWatcher(
			self.networkManager.asyncCall('GetDevices')
		)
		self._acquireInterfacesCall.finished.connect(self._acquireInterfaceData)
	def _acquireInterfaceData(self, reply):
		"""Continuation of __init__.
		
			[DDR 2019-11-05] Note: In Qt 5.7, we can't just go
			self._networkInterfaces['Device'].property(), like we could with
			self._networkInterfaces['Device'].call(), because .property()
			doesn't seem to work. afaik, it _should_ work, and the example in
			https://stackoverflow.com/questions/20042995/error-getting-dbus-interface-property-with-qdbusinterface
			which is directly relevant to our situation shows it working. Yet,
			I cannot figure out how to port it to Python. So we do it manually
			with the `'* property interface'`s.
					Note: https://doc.qt.io/archives/qt-5.7/qnetworkinterface.html is
			a thing. Shame it doesn't have notification events.
					Command-line based examples:
			docs: https://developer.gnome.org/NetworkManager/0.9/spec.html
			org.freedesktop.NetworkManager.Device.Wired has ipv4/6 properties
			gdbus introspect --system --dest org.freedesktop.NetworkManager.Device.Wired --object-path /org/freedesktop/NetworkManager/Device/Wired
			
			- Get network interfaces:
				> gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager
					- Has ActivateConnection method in org.freedesktop.NetworkManager
					- Has GetDevices method in org.freedesktop.NetworkManager
						> gdbus call --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager --method org.freedesktop.NetworkManager.GetDevices
							- ([objectpath '/org/freedesktop/NetworkManager/Devices/0', '/org/freedesktop/NetworkManager/Devices/1', '/org/freedesktop/NetworkManager/Devices/2', '/org/freedesktop/NetworkManager/Devices/3'],)
						> gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager/Devices/0
							- This is apparently a network connection - in this case, loopback.
							- Links to IPv4/6 config.
						> gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager/Devices/1
							- eth0
							- is plugged in?
								- org.freedesktop.NetworkManager.Device.Wired property Carrier
								- [implements g interfaces] Filter org.freedesktop.NetworkManager.GetDevices to get the list of plugged-in interfaces.
							> gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager/DHCP4Config/0
								- yields org.freedesktop.NetworkManager.DHCP4Config
								- from ip_address' Dhcp4Config property
								- [implements g n lan IPv4 or v6] in properties & PropertiesChanged signal.
							> gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager/IP6Config/2
								- yields org.freedesktop.NetworkManager.IP6Config
								- from ip_address' Ip6Config property
								- [implements g n g n www IPv4 or v6] in Addresses (first item) & PropertiesChanged signal.
							- has Disconnect method
								- https://developer.gnome.org/NetworkManager/0.9/spec.html#org.freedesktop.NetworkManager.Device.Disconnect
						> gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager/Devices/2
							- eth1
						> gdbus introspect --system --dest org.freedesktop.NetworkManager --object-path /org/freedesktop/NetworkManager/Devices/3
							- usb0
		"""
		
		reply = QDBusPendingReply(reply)
		if reply.isError():
			raise DBusException("%s: %s" % (reply.error().name(), reply.error().message()))
		reply = reply.value()
		
		self._networkInterfaces = [{
			'Device': QDBusInterface( #Provides node.
				f"org.freedesktop.NetworkManager", #Service
				devicePath, #Path
				f"org.freedesktop.NetworkManager.Device", #Interface
				QDBusConnection.systemBus(),
			),
			'Device.Wired': QDBusInterface( #Provides node.
				f"org.freedesktop.NetworkManager", #Service
				devicePath, #Path
				f"org.freedesktop.NetworkManager.Device.Wired", #Interface
				QDBusConnection.systemBus(),
			),
			'Device property interface': QDBusInterface( #Provides interface to get properties of previous node, because `.property()` is broken.
				"org.freedesktop.NetworkManager", #Service
				devicePath, #Path
				"org.freedesktop.DBus.Properties",#Interface
				QDBusConnection.systemBus()
			),
		} for devicePath in reply ]
		
		for interfaces in self._networkInterfaces:
			for networkInterface in interfaces.values():
				networkInterface.setTimeout(1000)
				if not networkInterface.isValid():
					log.critical(f"Error: Can not connect to NetworkManager at {networkInterface.service()}. ({networkInterface.lastError().name()}: {networkInterface.lastError().message()}) Try running `apt install network-manager`?")
					raise Exception("D-Bus Setup Error")
			
			#Deadlock fix as above.
			QDBusConnection.systemBus().registerObject(
				interfaces['Device'].path(), self )
			
			#Use above interface to look up the IP address interfaces.
			interfaces['Ip4Config'] = QDBusInterface( #Provides interface to get properties of previous node, because `.property()` is broken.
				"org.freedesktop.NetworkManager", #Service
				QDBusReply(interfaces['Device property interface'].call('Get', #Method
					'org.freedesktop.NetworkManager.Device', 'Ip4Config' )).value(), #Interface, Property → Path
				"org.freedesktop.NetworkManager.IP4Config", #Interface
				QDBusConnection.systemBus()
			)
			interfaces['Ip4Config property interface'] = QDBusInterface( #Provides interface to get properties of previous node, because `.property()` is broken.
				"org.freedesktop.NetworkManager", #Service
				interfaces['Ip4Config'].path(), #Path
				"org.freedesktop.DBus.Properties",#Interface
				QDBusConnection.systemBus()
			)
			
			interfaces['Ip6Config'] = QDBusInterface( #Provides interface to get properties of previous node, because `.property()` is broken.
				"org.freedesktop.NetworkManager", #Service
				QDBusReply(interfaces['Device property interface'].call('Get', #Method
					'org.freedesktop.NetworkManager.Device', 'Ip6Config' )).value(), #Interface, Property → Path
				"org.freedesktop.NetworkManager.IP6Config", #Interface
				QDBusConnection.systemBus()
			)
			interfaces['Ip6Config property interface'] = QDBusInterface( #Provides interface to get properties of previous node, because `.property()` is broken.
				"org.freedesktop.NetworkManager", #Service
				interfaces['Ip6Config'].path(), #Path
				"org.freedesktop.DBus.Properties",#Interface
				QDBusConnection.systemBus()
			)
			
			#Subscribe to network update signals, for ip address and carrier status.
			QDBusConnection.systemBus().connect(
				f"org.freedesktop.NetworkManager", #Service
				interfaces['Device'].path(),
				f"org.freedesktop.NetworkManager.Device", #Interface
				'PropertiesChanged', #Signal
				self.__interfacePropertiesChangedEvent,
			)
			QDBusConnection.systemBus().connect(
				f"org.freedesktop.NetworkManager", #Service
				interfaces['Device'].path(),
				f"org.freedesktop.NetworkManager.Device.Wired", #Interface
				'PropertiesChanged', #Signal
				self.__interfacePropertiesChangedEvent,
			)
			QDBusConnection.systemBus().connect( #untested, don't know how to change ip4 address
				f"org.freedesktop.NetworkManager", #Service
				QDBusReply(interfaces['Device property interface'].call('Get',
					'org.freedesktop.NetworkManager.Device', 'Dhcp4Config' ) ).value(), #Interface, Property → Path
				f"org.freedesktop.NetworkManager.Dhcp4Config", #Interface
				'PropertiesChanged', #Signal
				self.__interfacePropertiesChangedEvent,
			)
			QDBusConnection.systemBus().connect( #untested, don't know how to change ip6 address
				f"org.freedesktop.NetworkManager", #Service
				QDBusReply(interfaces['Device property interface'].call('Get',
					'org.freedesktop.NetworkManager.Device', 'Dhcp6Config' ) ).value(), #Interface, Property → Path
				f"org.freedesktop.NetworkManager.Dhcp6Config", #Interface
				'PropertiesChanged', #Signal
				self.__interfacePropertiesChangedEvent,
			)
			
		self.__rescan()
	
	def __getitem__(self, i):
		return self._connections[i]
	
	def __repr__(self):
		#pdb uses repr instad of str (which imo is more appropriate for an interactive debugging session)
		return f'{type(self)} ({self._connections})'
		
	
	def observe(self, callback):
		"""Add a function to get called when a volume is mounted or unmounted.
			
			The added function is immediately invoked."""
		
		assert callable(callback), f"Callback is not callable. (Expected function, got {callback}.)"
		self._callbacks += [callback]
		callback(self._connections)
	
	def unobserve(self, callback):
		"""Stop a function from getting called when a volume is mounted or unmounted."""
		
		assert callable(callback), f"Callback is not callable. (Expected function, got {callback}.)"
		self._callbacks = list(filter(
			lambda existingCallback: existingCallback != callback, 
			self._callbacks ))
	
	
	
	@pyqtSlot('QDBusMessage')
	def __interfacePropertiesChangedEvent(self, msg):
		log.info(f'Rescanning, network change detected. ({msg.arguments()})')
		self.__rescan()
	
	def __rescan(self):
		self._connections.clear()
		for interfaces in self._networkInterfaces:
			carrier = QDBusReply(
				interfaces['Device property interface'].call('Get',
					'org.freedesktop.NetworkManager.Device.Wired', 'Carrier' ) ) #Interface, Property
			if carrier.isValid() and carrier.value():
				try:
					addr = IPv4Address(
						QDBusReply(
							interfaces['Device property interface'].call(
								'Get', #Method
								'org.freedesktop.NetworkManager.Device', #Interface
								'Ip4Address', #Property
							)
						).value()
					)
					addr = IPv4Address('.'.join(reversed(str(addr).split('.')))) #So close. Truly, if there's two ways of representing information… (note: This is actually Python's fault here, the number parses fine in a browser address bar.)
				except AddressValueError:
					try:
						#"Array of tuples of IPv4 address/prefix/gateway. All 3 elements of each tuple are in network byte order. Essentially: [(addr, prefix, gateway), (addr, prefix, gateway), ...]"
						#	-- https://developer.gnome.org/NetworkManager/0.9/spec.html
						addr = IPv6Address(bytes(
							QDBusReply(
								interfaces['Ip6Config property interface'].call(
									'Get', #Method
									'org.freedesktop.NetworkManager.IP6Config', #Interface
									'Addresses', #Property
								)
							).value()[-1][0]
						))
					except (AddressValueError, IndexError):
						addr = None
				
				interface = QDBusReply(
					interfaces['Device property interface'].call(
						'Get', #Method
						'org.freedesktop.NetworkManager.Device', #Interface
						'Interface', #Property
					)
				).value()
				
				if addr:
					self._connections.append({
						'path': interfaces['Device'].path(),
						'name': defaultdict(
							lambda: 'generic connection', 
							{'e': 'ethernet', 'u': 'usb'}
						)[interface[0]],
						'address': addr,
					})
		
		log.info(f'conns: {self._connections}')
		
		for callback in self._callbacks:
			callback(self._connections)
コード例 #3
0
class ExternalPartitions(QObject):
	def __init__(self):
		"""
			Get _partitions, a list of things you can save video to.
			{
				"name": "Testdisk",
				"device": "mmcblk0p1",
				"uuid": "a14d610d-b524-4af2-9a1a-fa3dd1184258",
				"path": bytes("/dev/sda", 'utf8'),
				"size": 1294839100, #bytes, 64-bit positive integer
				"readOnly": False,
				"interface": "usb", #"usb" or "sd"
			}
		"""
		super().__init__()
		self._partitions = []
		
		#observers collection
		self._callbacks = []
		self.uDisks2ObjectManager = QDBusInterface(
			f"org.freedesktop.UDisks2", #Service
			f"/org/freedesktop/UDisks2", #Path
			f"org.freedesktop.DBus.ObjectManager", #Interface
			QDBusConnection.systemBus(),
		)
		self.uDisks2ObjectManager.setTimeout(10) #Set to 1000 after startup period.
		
		#Retry. This doesn't connect the first time, no matter what the time limit is. I don't know why, probably something in the start-on-demand logic.
		if not self.uDisks2ObjectManager.isValid():
			self.uDisks2ObjectManager = QDBusInterface(
				f"org.freedesktop.UDisks2", #Service
				f"/org/freedesktop/UDisks2", #Path
				f"org.freedesktop.DBus.ObjectManager", #Interface
				QDBusConnection.systemBus(),
			)
			self.uDisks2ObjectManager.setTimeout(10)
			
			if not self.uDisks2ObjectManager.isValid():
				log.critical(f"Error: Can not connect to udisks2 at {self.uDisks2ObjectManager.service()}. ({self.uDisks2ObjectManager.lastError().name()}: {self.uDisks2ObjectManager.lastError().message()}) Try running `apt install udisks2`?")
				raise Exception("D-Bus Setup Error")
		
		self.uDisks2ObjectManager.setTimeout(1000)
		
		
		#The .connect call freezes if we don't do this, or if we do this twice.
		#This bug was fixed by Qt 5.11.
		QDBusConnection.systemBus().registerObject(
			f"/org/freedesktop/UDisks2", 
			self,
		)
		
		QDBusConnection.systemBus().connect(
			f"org.freedesktop.UDisks2", #Service
			f"/org/freedesktop/UDisks2", #Path
			f"org.freedesktop.DBus.ObjectManager", #Interface
			'InterfacesAdded', #Signal
			self.__interfacesAddedEvent,
		)
		
		QDBusConnection.systemBus().connect(
			f"org.freedesktop.UDisks2", #Service
			f"/org/freedesktop/UDisks2", #Path
			f"org.freedesktop.DBus.ObjectManager", #Interface
			'InterfacesRemoved', #Signal
			self.__interfacesRemovedEvent,
		)	
		
		for name, data in QDBusReply(self.uDisks2ObjectManager.call('GetManagedObjects')).value().items():
			self.__interfacesAdded(name, data)
	
	def __getitem__(self, i):
		return self._partitions[i]
	
	def __repr__(self):
		#pdb uses repr instad of str (which imo is more appropriate for an interactive debugging session)
		return f'{type(self)} ({self._partitions})'
	
	def observe(self, callback):
		"""Add a function to get called when a volume is mounted or unmounted.
			
			The added function is immediately invoked."""
		
		assert callable(callback), f"Callback is not callable. (Expected function, got {callback}.)"
		self._callbacks += [callback]
		callback(self._partitions)
	
	def unobserve(self, callback):
		"""Stop a function from getting called when a volume is mounted or unmounted."""
		
		assert callable(callback), f"Callback is not callable. (Expected function, got {callback}.)"
		self._callbacks = list(filter(
			lambda existingCallback: existingCallback != callback, 
			self._callbacks ) )
	
	
	@pyqtSlot('QDBusMessage')
	def __interfacesAddedEvent(self, msg):
		self.__interfacesAdded(*msg.arguments())
	
	def __interfacesAdded(self, name, data):
		if 'org.freedesktop.UDisks2.Filesystem' in data:
			#"Now, for each file system which just got mounted, …"
			
			#Filter root, which is mounted on / and /media/mmcblk0p2.
			if len(data['org.freedesktop.UDisks2.Filesystem']['MountPoints']) != 1:
				return
			
			#Filter out whatever gets mounted to /boot.
			if not bytes(data['org.freedesktop.UDisks2.Filesystem']['MountPoints'][0]).startswith(b'/media/'):
				return
			
			log.debug(f"Partition mounted at {bytes(data['org.freedesktop.UDisks2.Filesystem']['MountPoints'][0]).decode('utf-8')}.") #toStdString() doesn't seem to exist, perhaps because we don't have std strings.
			
			self._partitions += [{
				'name': data['org.freedesktop.UDisks2.Block']['IdLabel'],
				'device': name,
				'uuid': data['org.freedesktop.UDisks2.Block']['IdUUID'], #Found at `/dev/disk/by-uuid/`.
				'path': bytes(data['org.freedesktop.UDisks2.Filesystem']['MountPoints'][0])[:-1], #Trim off a null byte at the end, we don't need it in python.
				'size': data['org.freedesktop.UDisks2.Block']['Size'], #number of bytes, 64-bit positive integer
				'readOnly': data['org.freedesktop.UDisks2.Block']['ReadOnly'],
				'interface': 'usb' if True in [b'usb' in symlink for symlink in data['org.freedesktop.UDisks2.Block']['Symlinks']] else 'other', #This data comes in one message earlier, but it would be enough complexity to link the two that it makes more sense to just string match here.
			}]
			for callback in self._callbacks:
				callback(self._partitions)
	
	
	@pyqtSlot('QDBusMessage')
	def __interfacesRemovedEvent(self, msg):
		self.__interfacesRemoved(*msg.arguments())
	
	def __interfacesRemoved(self, name, data):
		if 'org.freedesktop.UDisks2.Partition' == data[0]:
			#"Now, for each file system which just got removed, …"
			self._partitions = list(filter(
				lambda partition: partition["device"] != name, 
				self._partitions ) )
			for callback in self._callbacks:
				callback(self._partitions)
	
	
	def list(self):
		return self._partitions
	
	def usageFor(self, device: str, callback: Callable[[], Dict[str,int]]):
		for partition in self._partitions:
			if partition['device'] == device:
				df = subprocess.Popen(
					['df', partition['path'], '--output=avail,used'], #used+avail != 1k-blocks
					stdout=subprocess.PIPE,
					stderr=subprocess.DEVNULL )
				
				def checkDf(*, timeout):
					exitStatus = df.poll()
					if exitStatus is None: #Still running, check again later.
						#Standard clamped exponential decay. Keeps polling to a reasonable amount, frequent at first then low.
						delay(self, timeout, lambda:
							checkDf(timeout=max(1, timeout*2)) )
					elif exitStatus: #df failure, raise an error
						if exitStatus == 1:
							#When a storage device with multiple partitions is removed,
							#the observer fires once for each partition. This means
							#that, for one partition, the client will issue a spurious
							#call to this function with the stale partition's device.
							log.debug(f'Unknown device {device}.')
							log.debug(f'Known devices are {[p["device"] for p in self._partitions]}.')
						else:
							raise Exception(f'df exited with error {exitStatus}')
					else:
						info = ( #Chop up df command output.
							df.communicate()[0]
							.split(b'\n')[1] #Remove first line, column headings
							.split() #Each output is now in a list.
						)
						callback({
							'available': int(info[0]),
							'used': int(info[1]),
							'total': int(info[0]) + int(info[1]),
						})
				delay(self, 0.20, lambda: #Initial delay, df usually runs in .17s.
					checkDf(timeout=0.05) )
コード例 #4
0
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')
コード例 #5
0
ファイル: api.py プロジェクト: krontech/chronos-web-interface
if not QDBusConnection.systemBus().isConnected():
	print("Error: Can not connect to D-Bus. Is D-Bus itself running?", file=sys.stderr)
	raise Exception("D-Bus Setup Error")

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(),
コード例 #6
0
ファイル: api.py プロジェクト: krontech/chronos-gui-1
          file=sys.stderr)
    sys.exit(-1)

cameraControlAPI = QDBusInterface(
    'com.krontech.chronos.control',  #Service
    '/com/krontech/chronos/control',  #Path
    'com.krontech.chronos.control',  #Interface
    QDBusConnection.systemBus())
cameraVideoAPI = QDBusInterface(
    'com.krontech.chronos.video',  #Service
    '/com/krontech/chronos/video',  #Path
    'com.krontech.chronos.video',  #Interface
    QDBusConnection.systemBus())

cameraControlAPI.setTimeout(
    16
)  #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 should probably use events which are emitted as the event progresses. One frame (at 60fps) should be plenty of time for the API to respond, and also quick enough that we'll notice any slowness. The mock replies to messages in under 1ms, so I'm not too worried here.
cameraVideoAPI.setTimeout(16)

if not cameraControlAPI.isValid():
    print("Error: Can not connect to Camera D-Bus API at %s. (%s: %s)" % (
        cameraControlAPI.service(),
        cameraControlAPI.lastError().name(),
        cameraControlAPI.lastError().message(),
    ),
          file=sys.stderr)
    sys.exit(-2)
if not cameraVideoAPI.isValid():
    print("Error: Can not connect to Camera D-Bus API at %s. (%s: %s)" % (
        cameraVideoAPI.service(),
        cameraVideoAPI.lastError().name(),