diff --git a/src/calibre/devices/udisks.py b/src/calibre/devices/udisks.py index c8e5ee00b1..aadd031f54 100644 --- a/src/calibre/devices/udisks.py +++ b/src/calibre/devices/udisks.py @@ -5,9 +5,12 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import json import os import re +import subprocess from contextlib import suppress +from calibre.constants import isfreebsd def node_mountpoint(node): @@ -19,11 +22,18 @@ def node_mountpoint(node): return raw.replace(b'\\040', b' ').replace(b'\\011', b'\t').replace(b'\\012', b'\n').replace(b'\\0134', b'\\').decode('utf-8') - with open('/proc/mounts', 'rb') as src: - for line in src.readlines(): - line = line.split() - if line[0] == node: - return de_mangle(line[1]) + if isfreebsd: + cmd = subprocess.run(['mount', '-p', '--libxo', 'json'], capture_output=True, encoding='UTF-8') + stdout = json.loads(cmd.stdout) + for row in stdout['mount']['fstab']: + if (row['device'].encode('utf-8') == node): + return de_mangle(row['mntpoint'].encode('utf-8')) + else: + with open('/proc/mounts', 'rb') as src: + for line in src.readlines(): + line = line.split() + if line[0] == node: + return de_mangle(line[1]) return None @@ -37,6 +47,7 @@ class UDisks: BLOCK = f'{BUS_NAME}.Block' FILESYSTEM = f'{BUS_NAME}.Filesystem' DRIVE = f'{BUS_NAME}.Drive' + OBJECTMANAGER = 'org.freedesktop.DBus.ObjectManager' PATH = '/org/freedesktop/UDisks2' def __enter__(self): @@ -78,6 +89,37 @@ class UDisks: with suppress(Exception): yield devname, self.get_device_node_path(devname) + def find_device_vols_by_serial(self, serial): + from jeepney import DBusAddress, new_method_call + + def decodePath(encoded): + ret = '' + for c in encoded: + if (c != 0): + ret += str(c) + return ret + + drives = [] + blocks = [] + vols = [] + a = DBusAddress(self.PATH, bus_name=self.BUS_NAME, interface=self.OBJECTMANAGER) + msg = new_method_call(a, 'GetManagedObjects') + r = self.send(msg) + for k,v in r.body[0].items(): + if os.path.join(self.PATH, '/block_devices') in k: + blocks.append({'k': k, 'v': v.get(f'{self.BUS_NAME}.Block', {})}) + if os.path.join(self.PATH, '/drives') in k: + drive = v.get(f'{self.BUS_NAME}.Drive', {}) + if drive.get('ConnectionBus')[1] == 'usb' and drive.get('Removable')[1] and drive.get('Serial')[1] == serial: + drives.append(k) + for block in blocks: + if block['v']['Drive'][1] in drives: + vols.append({ + 'Block': block['k'], + 'Device': block['v']['Device'][1].decode('ascii').strip('\x00'), + }) + return vols + def device(self, device_node_path): device_node_path = os.path.realpath(device_node_path) devname = device_node_path.split('/')[-1] @@ -101,7 +143,8 @@ class UDisks: def mount(self, device_node_path): msg = self.filesystem_operation_message(device_node_path, 'Mount', options=('s', ','.join(basic_mount_options()))) try: - self.send(msg) + r = self.send(msg) + return r.body[0] except Exception: # May be already mounted, check mp = node_mountpoint(str(device_node_path)) @@ -130,6 +173,14 @@ class UDisks: },)) self.send(msg) + def rescan(self, device_node_path): + from jeepney import new_method_call + devname = self.device(device_node_path) + a = self.address(f'block_devices/{devname}', self.BLOCK) + msg = new_method_call(a, 'Rescan', 'a{sv}', ({ + 'auth.no_user_interaction': ('b', True), + },)) + self.send(msg) def get_udisks(): return UDisks() @@ -149,6 +200,13 @@ def umount(node_path): with get_udisks() as u: u.unmount(node_path) +def rescan(node_path): + with get_udisks() as u: + u.rescan(node_path) + +def find_device_vols_by_serial(serial): + with get_udisks() as u: + return u.find_device_vols_by_serial(serial) def test_udisks(): import sys diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index d4b79e9f26..ee61182b5c 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -20,7 +20,7 @@ from contextlib import suppress from itertools import repeat from calibre import prints -from calibre.constants import is_debugging, isfreebsd, islinux, ismacos, iswindows +from calibre.constants import DEBUG, is_debugging, isfreebsd, islinux, ismacos, iswindows from calibre.devices.errors import DeviceError from calibre.devices.interface import FAKE_DEVICE_SERIAL, DevicePlugin, ModelMetadata from calibre.devices.usbms.deviceconfig import DeviceConfig @@ -696,12 +696,14 @@ class Device(DeviceConfig, DevicePlugin): # open for FreeBSD # find the device node or nodes that match the S/N we already have from the scanner # and attempt to mount each one -# 1. get list of devices in /dev with matching s/n etc. +# 1. get list of devices via DBUS UDisk2 with matching s/n etc. # 2. get list of volumes associated with each -# 3. attempt to mount each one using Hal +# 3. attempt to mount each one using UDisks2 # 4. when finished, we have a list of mount points and associated dbus nodes # def open_freebsd(self): + from calibre.devices.udisks import find_device_vols_by_serial + # There should be some way to access the -v arg... verbose = False @@ -711,18 +713,80 @@ class Device(DeviceConfig, DevicePlugin): if not d.serial: raise DeviceError("Device has no S/N. Can't continue") - from .hal import get_hal - hal = get_hal() - vols = hal.get_volumes(d) + + vols = find_device_vols_by_serial(d.serial) + if verbose: print('FBSD:\t', vols) - ok, mv = hal.mount_volumes(vols) + ok, mv = self.freebsd_mount_volumes(vols) if not ok: raise DeviceError(_('Unable to mount the device')) for k, v in mv.items(): setattr(self, k, v) + def freebsd_mount_volumes(self, vols): + def fmount(node): + mp = self.node_mountpoint(node) + if mp is not None: + # Already mounted + return mp + + from calibre.devices.udisks import mount, rescan + for i in range(6): + try: + mp = mount(node) + break + except Exception as e: + if i < 5: + rescan(node) + time.sleep(1) + else: + print('Udisks mount call failed:') + import traceback + traceback.print_exc() + + return mp + + mp = None + mtd = 0 + ans = { + '_main_prefix': None, '_main_vol': None, + '_card_a_prefix': None, '_card_a_vol': None, + '_card_b_prefix': None, '_card_b_vol': None, + } + for vol in vols: + try: + mp = fmount(vol['Device']) + except Exception as e: + print('Failed to mount: ' + vol['Device']) + import traceback + traceback.print_exc() + + if mp is None: + continue + + # Mount Point becomes Mount Path + mp += '/' + if DEBUG: + print('FBSD:\tmounted', vol['Device'], 'on', mp) + if mtd == 0: + ans['_main_prefix'], ans['_main_vol'] = mp, vol['Device'] + if DEBUG: + print('FBSD:\tmain = ', mp) + elif mtd == 1: + ans['_card_a_prefix'], ans['_card_a_vol'] = mp, vol['Device'] + if DEBUG: + print('FBSD:\tcard a = ', mp) + elif mtd == 2: + ans['_card_b_prefix'], ans['_card_b_vol'] = mp, vol['Device'] + if DEBUG: + print('FBSD:\tcard b = ', mp) + break + mtd += 1 + + return mtd > 0, ans + # # ------------------------------------------------------ # @@ -731,14 +795,13 @@ class Device(DeviceConfig, DevicePlugin): # mounted filesystems, using the stored volume object # def eject_freebsd(self): - from .hal import get_hal - hal = get_hal() + from calibre.devices.udisks import umount if self._main_prefix: - hal.unmount(self._main_vol) + umount(self._main_vol) if self._card_a_prefix: - hal.unmount(self._card_a_vol) + umount(self._card_a_vol) if self._card_b_prefix: - hal.unmount(self._card_b_vol) + umount(self._card_b_vol) self._main_prefix = self._main_vol = None self._card_a_prefix = self._card_a_vol = None @@ -786,11 +849,7 @@ class Device(DeviceConfig, DevicePlugin): self.open_linux() if isfreebsd: self._main_vol = self._card_a_vol = self._card_b_vol = None - try: - self.open_freebsd() - except DeviceError: - time.sleep(2) - self.open_freebsd() + self.open_freebsd() if iswindows: self.open_windows() if ismacos: diff --git a/src/calibre/devices/usbms/hal.py b/src/calibre/devices/usbms/hal.py deleted file mode 100644 index f6ca5e87f6..0000000000 --- a/src/calibre/devices/usbms/hal.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python -# License: GPL v3 Copyright: 2021, Kovid Goyal - -import time - -from jeepney import DBusAddress, DBusErrorResponse, MessageType, Properties, new_method_call -from jeepney.io.blocking import open_dbus_connection - -from calibre.constants import DEBUG - - -class HAL: - - def __init__(self): - self.bus = open_dbus_connection('SYSTEM') - - def send(self, msg): - reply = self.bus.send_and_get_reply(msg) - if reply.header.message_type is MessageType.error: - raise DBusErrorResponse(reply) - return reply.body[0] - - def call(self, addr, method, sig='', *args): - if sig: - return self.send(new_method_call(addr, method, sig, args)) - return self.send(new_method_call(addr, method)) - - def prop(self, addr, name): - return self.send(Properties(addr).get(name)) - - def addr(self, path, interface): - return DBusAddress(path, bus_name='org.freedesktop.Hal', interface=f'org.freedesktop.Hal.{interface}') - - def get_volume(self, vpath): - vdevif = self.addr(vpath, 'Device') - if not self.prop(vdevif, 'block.is_volume') or self.prop(vdevif, 'volume.fsusage') != 'filesystem': - return - volif = self.addr(vpath, 'Volume') - pdevif = self.addr(self.prop(volif, 'info.parent'), 'Device') - return {'node': self.prop(pdevif, 'block.device'), - 'dev': vdevif, - 'vol': volif, - 'label': self.prop(vdevif, 'volume.label')} - - def get_volumes(self, d): - vols = [] - manager = self.addr('/org/freedesktop/Hal/Manager', 'Manager') - paths = self.call(manager, 'FindDeviceStringMatch', 'ss', 'usb.serial', d.serial) - for path in paths: - objif = self.addr(path, 'Device') - - # Extra paranoia... - try: - if d.idVendor == self.prop(objif, 'usb.vendor_id') and \ - d.idProduct == self.prop(objif, 'usb.product_id') and \ - d.manufacturer == self.prop(objif, 'usb.vendor') and \ - d.product == self.prop(objif, 'usb.product') and \ - d.serial == self.prop(objif, 'usb.serial'): - midpath = self.call(manager, 'FindDeviceStringMatch', 'ss', 'info.parent', path) - dpaths = self.call(manager, 'FindDeviceStringMatch', 'ss', 'storage.originating_device', path - ) + self.call(manager, 'FindDeviceStringMatch', 'ss', 'storage.originating_device', midpath[0]) - for dpath in dpaths: - try: - vpaths = self.call(manager, 'FindDeviceStringMatch', 'block.storage_device', dpath) - for vpath in vpaths: - try: - vol = self.get_volume(vpath) - if vol is not None: - vols.append(vol) - except DBusErrorResponse as e: - print(e) - continue - except DBusErrorResponse as e: - print(e) - continue - except DBusErrorResponse: - continue - vols.sort(key=lambda x: x['node']) - return vols - - def get_mount_point(self, vol): - if not self.prop(vol['dev'], 'volume.is_mounted'): - fstype = self.prop(vol['dev'], 'volume.fstype') - self.call(vol['vol'], 'Mount', 'ssas', 'Calibre-'+vol['label'], fstype, []) - loops = 0 - while not self.prop(vol['dev'], 'volume.is_mounted'): - time.sleep(1) - loops += 1 - if loops > 100: - raise Exception('ERROR: Timeout waiting for mount to complete') - return self.prop(vol['dev'], 'volume.mount_point') - - def mount_volumes(self, volumes): - mtd=0 - ans = { - '_main_prefix': None, '_main_vol': None, - '_card_a_prefix': None, '_card_a_vol': None, - '_card_b_prefix': None, '_card_b_vol': None, - } - for vol in volumes: - try: - mp = self.get_mount_point(vol) - except Exception as e: - print("Failed to mount: {vol['label']}", e) - continue - # Mount Point becomes Mount Path - mp += '/' - if DEBUG: - print('FBSD:\tmounted', vol['label'], 'on', mp) - if mtd == 0: - ans['_main_prefix'], ans['_main_vol'] = mp, vol['vol'] - if DEBUG: - print('FBSD:\tmain = ', mp) - elif mtd == 1: - ans['_card_a_prefix'], ans['_card_a_vol'] = mp, vol['vol'] - if DEBUG: - print('FBSD:\tcard a = ', mp) - elif mtd == 2: - ans['_card_b_prefix'], ans['_card_b_vol'] = mp, vol['vol'] - if DEBUG: - print('FBSD:\tcard b = ', mp) - break - mtd += 1 - - return mtd > 0, ans - - def unmount(self, vol): - try: - self.call(vol, 'Unmount', 'as', []) - except DBusErrorResponse as e: - print('Unable to eject ', e) - - -def get_hal(): - if not hasattr(get_hal, 'ans'): - get_hal.ans = HAL() - return get_hal.ans