mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Implement FreeBSD support using UDisk2
This commit is contained in:
parent
b4511ec429
commit
56c3b268db
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
|
Loading…
x
Reference in New Issue
Block a user