From 6f3987721824d02a425578561ee5da49a4458607 Mon Sep 17 00:00:00 2001
From: Guido Falsi <mad@madpilot.net>
Date: Thu, 13 Feb 2025 17:46:43 +0100
Subject: [PATCH] Implement FreeBSD support using UDisk2

---
 src/calibre/devices/udisks.py       |  70 ++++++++++++--
 src/calibre/devices/usbms/device.py |  93 +++++++++++++++----
 src/calibre/devices/usbms/hal.py    | 137 ----------------------------
 3 files changed, 140 insertions(+), 160 deletions(-)
 delete mode 100644 src/calibre/devices/usbms/hal.py

diff --git a/src/calibre/devices/udisks.py b/src/calibre/devices/udisks.py
index c8e5ee00b1..aadd031f54 100644
--- src/calibre/devices/udisks.py
+++ src/calibre/devices/udisks.py
@@ -5,9 +5,12 @@
 __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
 __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 de_mangle(raw):
         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 @@ def iter_block_devices(self):
             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 @@ def filesystem_operation_message(self, device_node_path, function_name, **kw):
     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 @@ def eject(self, device_node_path):
         },))
         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
--- src/calibre/devices/usbms/device.py
+++ src/calibre/devices/usbms/device.py
@@ -20,7 +20,7 @@
 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 @@ def is_readonly(mp):
 #  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 @@ def open_freebsd(self):
 
         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 @@ def open_freebsd(self):
 #        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 @@ def open(self, connected_device, library_uuid):
                     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:
-- 
2.49.0

