Implement FreeBSD support using UDisk2

This commit is contained in:
Guido Falsi 2025-02-13 17:46:43 +01:00
parent b4511ec429
commit 56c3b268db
3 changed files with 140 additions and 160 deletions

View File

@ -5,9 +5,12 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import json
import os import os
import re import re
import subprocess
from contextlib import suppress from contextlib import suppress
from calibre.constants import isfreebsd
def node_mountpoint(node): def node_mountpoint(node):
@ -19,6 +22,13 @@ def node_mountpoint(node):
return raw.replace(b'\\040', b' ').replace(b'\\011', b'\t').replace(b'\\012', return raw.replace(b'\\040', b' ').replace(b'\\011', b'\t').replace(b'\\012',
b'\n').replace(b'\\0134', b'\\').decode('utf-8') b'\n').replace(b'\\0134', b'\\').decode('utf-8')
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: with open('/proc/mounts', 'rb') as src:
for line in src.readlines(): for line in src.readlines():
line = line.split() line = line.split()
@ -37,6 +47,7 @@ class UDisks:
BLOCK = f'{BUS_NAME}.Block' BLOCK = f'{BUS_NAME}.Block'
FILESYSTEM = f'{BUS_NAME}.Filesystem' FILESYSTEM = f'{BUS_NAME}.Filesystem'
DRIVE = f'{BUS_NAME}.Drive' DRIVE = f'{BUS_NAME}.Drive'
OBJECTMANAGER = 'org.freedesktop.DBus.ObjectManager'
PATH = '/org/freedesktop/UDisks2' PATH = '/org/freedesktop/UDisks2'
def __enter__(self): def __enter__(self):
@ -78,6 +89,37 @@ class UDisks:
with suppress(Exception): with suppress(Exception):
yield devname, self.get_device_node_path(devname) 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): def device(self, device_node_path):
device_node_path = os.path.realpath(device_node_path) device_node_path = os.path.realpath(device_node_path)
devname = device_node_path.split('/')[-1] devname = device_node_path.split('/')[-1]
@ -101,7 +143,8 @@ class UDisks:
def mount(self, device_node_path): def mount(self, device_node_path):
msg = self.filesystem_operation_message(device_node_path, 'Mount', options=('s', ','.join(basic_mount_options()))) msg = self.filesystem_operation_message(device_node_path, 'Mount', options=('s', ','.join(basic_mount_options())))
try: try:
self.send(msg) r = self.send(msg)
return r.body[0]
except Exception: except Exception:
# May be already mounted, check # May be already mounted, check
mp = node_mountpoint(str(device_node_path)) mp = node_mountpoint(str(device_node_path))
@ -130,6 +173,14 @@ class UDisks:
},)) },))
self.send(msg) 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(): def get_udisks():
return UDisks() return UDisks()
@ -149,6 +200,13 @@ def umount(node_path):
with get_udisks() as u: with get_udisks() as u:
u.unmount(node_path) 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(): def test_udisks():
import sys import sys

View File

@ -20,7 +20,7 @@ from contextlib import suppress
from itertools import repeat from itertools import repeat
from calibre import prints 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.errors import DeviceError
from calibre.devices.interface import FAKE_DEVICE_SERIAL, DevicePlugin, ModelMetadata from calibre.devices.interface import FAKE_DEVICE_SERIAL, DevicePlugin, ModelMetadata
from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.deviceconfig import DeviceConfig
@ -696,12 +696,14 @@ class Device(DeviceConfig, DevicePlugin):
# open for FreeBSD # open for FreeBSD
# find the device node or nodes that match the S/N we already have from the scanner # find the device node or nodes that match the S/N we already have from the scanner
# and attempt to mount each one # 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 # 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 # 4. when finished, we have a list of mount points and associated dbus nodes
# #
def open_freebsd(self): def open_freebsd(self):
from calibre.devices.udisks import find_device_vols_by_serial
# There should be some way to access the -v arg... # There should be some way to access the -v arg...
verbose = False verbose = False
@ -711,18 +713,80 @@ class Device(DeviceConfig, DevicePlugin):
if not d.serial: if not d.serial:
raise DeviceError("Device has no S/N. Can't continue") raise DeviceError("Device has no S/N. Can't continue")
from .hal import get_hal
hal = get_hal() vols = find_device_vols_by_serial(d.serial)
vols = hal.get_volumes(d)
if verbose: if verbose:
print('FBSD:\t', vols) print('FBSD:\t', vols)
ok, mv = hal.mount_volumes(vols) ok, mv = self.freebsd_mount_volumes(vols)
if not ok: if not ok:
raise DeviceError(_('Unable to mount the device')) raise DeviceError(_('Unable to mount the device'))
for k, v in mv.items(): for k, v in mv.items():
setattr(self, k, v) 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 # mounted filesystems, using the stored volume object
# #
def eject_freebsd(self): def eject_freebsd(self):
from .hal import get_hal from calibre.devices.udisks import umount
hal = get_hal()
if self._main_prefix: if self._main_prefix:
hal.unmount(self._main_vol) umount(self._main_vol)
if self._card_a_prefix: if self._card_a_prefix:
hal.unmount(self._card_a_vol) umount(self._card_a_vol)
if self._card_b_prefix: if self._card_b_prefix:
hal.unmount(self._card_b_vol) umount(self._card_b_vol)
self._main_prefix = self._main_vol = None self._main_prefix = self._main_vol = None
self._card_a_prefix = self._card_a_vol = None self._card_a_prefix = self._card_a_vol = None
@ -786,10 +849,6 @@ class Device(DeviceConfig, DevicePlugin):
self.open_linux() self.open_linux()
if isfreebsd: if isfreebsd:
self._main_vol = self._card_a_vol = self._card_b_vol = None 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: if iswindows:
self.open_windows() self.open_windows()

View File

@ -1,137 +0,0 @@
#!/usr/bin/env python
# License: GPL v3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
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