util.py 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. platform.darwin.util: utilities for the macOS platform
  12. """
  13. from pathlib import Path
  14. from subprocess import run, PIPE, DEVNULL
  15. from ...obj import MMGenLabel
  16. from ...util import oneshot_warning
  17. def get_device_size(path_or_label):
  18. import re
  19. cp = run(['diskutil', 'info', path_or_label], text=True, stdout=PIPE, check=True)
  20. res = [e for e in cp.stdout.splitlines() if 'Disk Size' in e]
  21. errmsg = '‘diskutil info’ output could not be parsed for device size'
  22. assert len(res) == 1, f'{errmsg}:\n{cp.stdout}'
  23. m = re.search(r'\((\d+) Bytes\)', res[0])
  24. assert m, f'{errmsg}:\n{res[0]}'
  25. return int(m[1])
  26. class warn_ramdisk_too_small(oneshot_warning):
  27. message = 'requested ramdisk size ({}MB) too small, using {}MB instead'
  28. color = 'yellow'
  29. def __init__(self, usr_size, min_size):
  30. oneshot_warning.__init__(self, div=usr_size, fmt_args=[usr_size, min_size])
  31. class RamDiskLabel(MMGenLabel):
  32. max_len = 24
  33. desc = 'ramdisk label'
  34. class MacOSRamDisk:
  35. desc = 'ramdisk'
  36. min_size = 10 # 10MB is the minimum supported by hdiutil
  37. def __init__(self, cfg, label, size, *, path=None):
  38. if size < self.min_size:
  39. warn_ramdisk_too_small(size, self.min_size)
  40. size = self.min_size
  41. self.cfg = cfg
  42. self.label = RamDiskLabel(label)
  43. self.size = size # size in MiB
  44. self.dfl_path = Path('/Volumes') / self.label
  45. self.path = Path(path) if path else self.dfl_path
  46. def exists(self):
  47. return self.path.is_mount()
  48. def get_diskutil_size(self):
  49. return get_device_size(self.label) // (2**20)
  50. def create(self, *, quiet=False):
  51. redir = DEVNULL if quiet else None
  52. if self.exists():
  53. diskutil_size = self.get_diskutil_size()
  54. if diskutil_size != self.size:
  55. self.cfg._util.qmsg(f'Existing ramdisk has incorrect size {diskutil_size}MB, deleting')
  56. self.destroy()
  57. else:
  58. self.cfg._util.qmsg(f'{self.desc.capitalize()} {self.label.hl()} at path {self.path} already exists')
  59. return
  60. self.cfg._util.qmsg(f'Creating {self.desc} {self.label.hl()} of size {self.size}MB')
  61. cp = run(
  62. ['hdiutil', 'attach', '-nomount', f'ram://{2048 * self.size}'],
  63. stdout = PIPE,
  64. text = True,
  65. check = True)
  66. self.dev_name = cp.stdout.strip()
  67. self.cfg._util.qmsg(f'Created {self.desc} {self.label.hl()} [{self.dev_name}]')
  68. run(['diskutil', 'eraseVolume', 'APFS', self.label, self.dev_name], stdout=redir, check=True)
  69. diskutil_size = self.get_diskutil_size()
  70. assert diskutil_size == self.size, 'Reported ramdisk size {diskutil_size}MB is incorrect!'
  71. if self.path != self.dfl_path:
  72. run(['diskutil', 'umount', self.label], stdout=redir, check=True)
  73. self.path.mkdir(parents=True, exist_ok=True)
  74. run(['diskutil', 'mount', '-mountPoint', str(self.path.absolute()), self.label], stdout=redir, check=True)
  75. def destroy(self, *, quiet=False):
  76. if not self.exists():
  77. self.cfg._util.qmsg(f'{self.desc.capitalize()} {self.label.hl()} at path {self.path} not found')
  78. return
  79. redir = DEVNULL if quiet else None
  80. run(['diskutil', 'eject', self.label], stdout=redir, check=True)
  81. if not quiet:
  82. self.cfg._util.qmsg(f'Destroyed {self.desc} {self.label.hl()} at {self.path}')