From a24eed08264911b642afefab10a014c3ec6f90d2 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 26 Aug 2024 14:44:09 +0000 Subject: [PATCH] macOS: support autosign/automount for BTC Testing: $ test/cmdtest.py autosign_clean autosign_automount autosign_btc --- mmgen/autosign.py | 15 +++++++ mmgen/data/release_date | 2 +- mmgen/data/version | 2 +- mmgen/main_autosign.py | 11 ++--- mmgen/platform/__init__.py | 0 mmgen/platform/darwin/__init__.py | 0 mmgen/platform/darwin/util.py | 54 +++++++++++++++++++++++++ setup.cfg | 2 + test/cmdtest_py_d/ct_autosign.py | 39 +++++++++++++++++- test/overlay/fakemods/mmgen/autosign.py | 1 + 10 files changed, 117 insertions(+), 9 deletions(-) create mode 100755 mmgen/platform/__init__.py create mode 100755 mmgen/platform/darwin/__init__.py create mode 100755 mmgen/platform/darwin/util.py diff --git a/mmgen/autosign.py b/mmgen/autosign.py index a9a2310e..9b58a1f9 100755 --- a/mmgen/autosign.py +++ b/mmgen/autosign.py @@ -325,6 +325,7 @@ class Autosign: dev_label = 'MMGEN_TX' linux_mount_subdir = 'mmgen_autosign' + macOS_ramdisk_name = 'AutosignRamDisk' wallet_subdir = 'autosign' mn_fmts = { @@ -374,6 +375,9 @@ class Autosign: to a directory! Please create the mountpoint and add an entry to your fstab as described in this script’s help text. """ + elif sys.platform == 'darwin': + self.dfl_mountpoint = f'/Volumes/{self.dev_label}' + self.dfl_shm_dir = f'/Volumes/{self.macOS_ramdisk_name}' self.cfg = cfg @@ -385,12 +389,18 @@ class Autosign: if sys.platform == 'linux': self.mount_cmd = f'mount {self.mountpoint}' self.umount_cmd = f'umount {self.mountpoint}' + elif sys.platform == 'darwin': + self.mount_cmd = f'diskutil mount {self.dev_label}' + self.umount_cmd = f'diskutil eject {self.dev_label}' self.init_fixup() # these use the ‘fixed-up’ values: if sys.platform == 'linux': self.dev_label_path = self.dev_label_dir / self.dev_label + elif sys.platform == 'darwin': + from .platform.darwin.util import MacOSRamDisk + self.ramdisk = MacOSRamDisk(cfg, self.macOS_ramdisk_name, 10, path=self.shm_dir) self.keyfile = self.mountpoint / 'autosign.key' @@ -621,6 +631,9 @@ class Autosign: except: die(2,f"Unable to create wallet directory '{self.wallet_dir}'") + if sys.platform == 'darwin': + self.ramdisk.create() + remove_wallet_dir() create_wallet_dir() self.gen_key(no_unmount=True) @@ -717,6 +730,8 @@ class Autosign: return True if sys.platform == 'linux': return self.dev_label_path.exists() + elif sys.platform == 'darwin': + return self.mountpoint.exists() async def main_loop(self): if not self.cfg.stealth_led: diff --git a/mmgen/data/release_date b/mmgen/data/release_date index 7a5ff022..bbf9c7cc 100644 --- a/mmgen/data/release_date +++ b/mmgen/data/release_date @@ -1 +1 @@ -July 2024 +August 2024 diff --git a/mmgen/data/version b/mmgen/data/version index fbd8321d..db4e727a 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -15.0.dev2 +15.0.dev3 diff --git a/mmgen/main_autosign.py b/mmgen/main_autosign.py index e52d5d37..a024dafb 100755 --- a/mmgen/main_autosign.py +++ b/mmgen/main_autosign.py @@ -87,12 +87,13 @@ On supported platforms (currently Orange Pi, Rock Pi and Raspberry Pi boards), the status LED indicates whether the program is busy or in standby mode, i.e. ready for device insertion or removal. -The removable device must have a partition labeled MMGEN_TX with a user- -writable root directory. +The removable device must have a partition with a filesystem labeled MMGEN_TX +and a user-writable root directory. For interoperability between OS-es, it’s +recommended to use the exFAT file system. On both the signing and online machines the mountpoint ‘{asi.mountpoint}’ -(as currently configured) must exist and ‘/etc/fstab’ must contain the -following entry: +(as currently configured) must exist. Linux (not macOS) machines must have +an ‘/etc/fstab’ with the following entry: LABEL=MMGEN_TX {asi.mountpoint} auto noauto,user 0 0 @@ -118,7 +119,7 @@ file path to the end of the command line. Multiple temporary wallets may be created in this way and used for signing (note, however, that for XMR operations only one wallet is supported). -Autosigning is currently available only on Linux-based platforms. +Autosigning is currently supported on Linux and macOS only. SECURITY NOTE diff --git a/mmgen/platform/__init__.py b/mmgen/platform/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/mmgen/platform/darwin/__init__.py b/mmgen/platform/darwin/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/mmgen/platform/darwin/util.py b/mmgen/platform/darwin/util.py new file mode 100755 index 00000000..60520634 --- /dev/null +++ b/mmgen/platform/darwin/util.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2024 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +platform.darwin.util: utilities for the macOS platform +""" + +from pathlib import Path +from subprocess import run, PIPE, DEVNULL + +from ...color import cyan +from ...obj import MMGenLabel + +class RamDiskLabel(MMGenLabel): + max_len = 24 + desc = 'ramdisk label' + +class MacOSRamDisk: + + desc = 'macOS ramdisk' + + def __init__(self, cfg, label, size_in_MB, path=None): + self.cfg = cfg + self.label = RamDiskLabel(label) + self.size_in_MB = size_in_MB + self.dfl_path = Path('/Volumes') / self.label + self.path = Path(path) if path else self.dfl_path + + def create(self, quiet=False): + redir = DEVNULL if quiet else None + if self.path.exists(): + self.cfg._util.qmsg('{} {} [{}] already exists'.format(self.desc, self.label.hl(), self.path)) + return + cp = run(['hdiutil', 'attach', '-nomount', f'ram://{2048 * self.size_in_MB}'], stdout=PIPE, check=True) + self.dev_name = cp.stdout.decode().strip() + self.cfg._util.qmsg('{} {} [{}]'.format(cyan(f'Created {self.desc}'), self.label.hl(), self.dev_name)) + run(['diskutil', 'eraseVolume', 'APFS', self.label, self.dev_name], stdout=redir, check=True) + if self.path != self.dfl_path: + run(['diskutil', 'umount', self.label], stdout=redir, check=True) + self.path.mkdir(parents=True, exist_ok=True) + run(['diskutil', 'mount', '-mountPoint', str(self.path.absolute()), self.label], stdout=redir, check=True) + + def destroy(self, quiet=False): + redir = DEVNULL if quiet else None + run(['diskutil', 'eject', self.label], stdout=redir, check=True) + if not quiet: + self.cfg._util.qmsg('{} {} [{}]'.format(cyan(f'Destroyed {self.desc}'), self.label.hl(), self.path)) diff --git a/setup.cfg b/setup.cfg index 4b8edbf6..8e7a8921 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,8 @@ packages = mmgen.contrib mmgen.data mmgen.help + mmgen.platform + mmgen.platform.darwin mmgen.proto mmgen.proto.bch mmgen.proto.btc diff --git a/test/cmdtest_py_d/ct_autosign.py b/test/cmdtest_py_d/ct_autosign.py index d1683fdb..d1e5303c 100755 --- a/test/cmdtest_py_d/ct_autosign.py +++ b/test/cmdtest_py_d/ct_autosign.py @@ -20,7 +20,7 @@ test.cmdtest_py_d.ct_autosign: Autosign tests for the cmdtest.py test suite """ -import sys, os, time, shutil +import sys, os, time, shutil, atexit from subprocess import run,DEVNULL from pathlib import Path @@ -53,7 +53,7 @@ class CmdTestAutosignBase(CmdTestBase): networks = ('btc',) tmpdir_nums = [18] color = True - platform_skip = ('win32', 'darwin') + platform_skip = ('win32',) threaded = False daemon_coins = [] @@ -72,6 +72,9 @@ class CmdTestAutosignBase(CmdTestBase): if not (cfg.skipping_deps or self.live): self._create_removable_device() + if sys.platform == 'darwin' and not cfg.no_daemon_stop: + atexit.register(self._macOS_eject_disk, self.asi.dev_label) + self.opts = ['--coins='+','.join(self.coins)] if not self.live: @@ -83,6 +86,9 @@ class CmdTestAutosignBase(CmdTestBase): def __del__(self): if hasattr(self,'have_dummy_control_files'): LEDControl.delete_dummy_control_files() + if sys.platform == 'darwin' and not cfg.no_daemon_stop: + for label in (self.asi.dev_label, self.asi.ramdisk.label): + self._macOS_eject_disk(label) def _create_autosign_instances(self,create_dirs): d = {'offline': {'name':'asi'}} @@ -105,7 +111,12 @@ class CmdTestAutosignBase(CmdTestBase): for k in ('mountpoint', 'shm_dir', 'wallet_dir', 'dev_label_dir'): if subdir == 'online' and k in ('shm_dir', 'wallet_dir'): continue + if sys.platform == 'darwin' and k != 'mountpoint': + continue getattr(asi, k).mkdir(parents=True, exist_ok=True) + if sys.platform == 'darwin' and k == 'mountpoint': + asi.mountpoint.rmdir() + continue setattr(self, data['name'], asi) @@ -116,6 +127,20 @@ class CmdTestAutosignBase(CmdTestBase): run(f'truncate --size=10M {img}'.split(), check=True) run(f'/sbin/mkfs.ext2 -E root_owner={os.getuid()}:{os.getgid()} {img}'.split(), stdout=redir, stderr=redir, check=True) + elif sys.platform == 'darwin': + redir = None if cfg.exact_output or cfg.verbose else DEVNULL + run(f'hdiutil create -size 10M -fs exFAT -volname {self.asi.dev_label} {img}'.split(), + stdout=redir, check=True) + + def _macOS_mount_fs_image(self, loc): + time.sleep(0.2) + run(f'hdiutil attach -mountpoint {loc.mountpoint} {loc.fs_image_path}.dmg'.split(), stdout=DEVNULL, check=True) + + def _macOS_eject_disk(self, label): + try: + run(['diskutil' ,'eject', label], stdout=DEVNULL, stderr=DEVNULL) + except: + pass def start_daemons(self): self.spawn('',msg_only=True) @@ -140,6 +165,9 @@ class CmdTestAutosignBase(CmdTestBase): mn_desc = mn_type or 'default' mn_type = mn_type or 'mmgen' + if sys.platform == 'darwin' and not cfg.no_daemon_stop: + self._macOS_eject_disk(self.asi.ramdisk.label) + self.insert_device() t = self.spawn( @@ -181,6 +209,9 @@ class CmdTestAutosignBase(CmdTestBase): t.read() self.remove_device() + if sys.platform == 'darwin' and not cfg.no_daemon_stop: + atexit.register(self._macOS_eject_disk, self.asi.ramdisk.label) + return t def insert_device(self, asi='asi'): @@ -189,6 +220,8 @@ class CmdTestAutosignBase(CmdTestBase): loc = getattr(self, asi) if sys.platform == 'linux': loc.dev_label_path.touch() + elif sys.platform == 'darwin': + self._macOS_mount_fs_image(loc) def remove_device(self, asi='asi'): if self.live: @@ -197,6 +230,8 @@ class CmdTestAutosignBase(CmdTestBase): if sys.platform == 'linux': if loc.dev_label_path.exists(): loc.dev_label_path.unlink() + elif sys.platform == 'darwin': + loc.do_umount(silent=True) def _mount_ops(self, loc, cmd, *args, **kwargs): return getattr(getattr(self,loc),cmd)(*args, silent=self.silent_mount, **kwargs) diff --git a/test/overlay/fakemods/mmgen/autosign.py b/test/overlay/fakemods/mmgen/autosign.py index 2797825f..6b806f3b 100644 --- a/test/overlay/fakemods/mmgen/autosign.py +++ b/test/overlay/fakemods/mmgen/autosign.py @@ -18,4 +18,5 @@ class overlay_fake_Autosign: Autosign.dev_label = 'MMGEN_TS_TX' Autosign.linux_mount_subdir = 'mmgen_ts_autosign' +Autosign.macOS_ramdisk_name = 'TestAutosignRamDisk' Autosign.init_fixup = overlay_fake_Autosign.init_fixup