Browse Source

macOS: support autosign/automount for BTC

Testing:

    $ test/cmdtest.py autosign_clean autosign_automount autosign_btc
The MMGen Project 6 months ago
parent
commit
a24eed0826

+ 15 - 0
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:

+ 1 - 1
mmgen/data/release_date

@@ -1 +1 @@
-July 2024
+August 2024

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.0.dev2
+15.0.dev3

+ 6 - 5
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

+ 0 - 0
mmgen/platform/__init__.py


+ 0 - 0
mmgen/platform/darwin/__init__.py


+ 54 - 0
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 <mmgen@tuta.io>
+# 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))

+ 2 - 0
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

+ 37 - 2
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)

+ 1 - 0
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