devices.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. ########################################################################
  2. # Wiizard - A Wii games manager
  3. # Copyright (C) 2023 CYBERDEViL
  4. #
  5. # This file is part of Wiizard.
  6. #
  7. # Wiizard is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Wiizard is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. import pyudev
  22. import subprocess
  23. import time
  24. from PyQt5.QtCore import (
  25. pyqtSignal,
  26. QObject
  27. )
  28. from wiizard.thread import AbstractThread, THREAD_FLAG_IS_STOPABLE
  29. import pyblockinfo
  30. import pywwt
  31. class DeviceModel(QObject):
  32. """ Class to describe a storage device (partition)
  33. """
  34. deviceRemoved = pyqtSignal()
  35. def __init__(self, dev, devType):
  36. QObject.__init__(self)
  37. self.__device = dev
  38. self.__type = devType # 'disc' or 'partition'
  39. self.__vendor = ""
  40. self.__model = ""
  41. try:
  42. self.__vendor, self.__model = pyblockinfo.getVendorModel(dev)
  43. except pyblockinfo.error:
  44. print(f"!! Failed to get vendor and model for {dev}")
  45. @property
  46. def path(self):
  47. return self.__device
  48. @property
  49. def type(self):
  50. return self.__type
  51. @property
  52. def vendor(self):
  53. return self.__vendor
  54. @property
  55. def model(self):
  56. return self.__model
  57. def isMounted(self):
  58. try:
  59. output = subprocess.check_output("mount", shell=False)
  60. except subprocess.CalledProcessError as err:
  61. raise Exception("cmd 'mount' failed", err)
  62. for line in output.decode("utf-8").split("\n"):
  63. if not line:
  64. continue
  65. spline = line.split(" ")
  66. if self.__device == spline[0]:
  67. return True
  68. return False
  69. def getFsType(self):
  70. try:
  71. blkinfo = pyblockinfo.getBlkidValues(self.path)
  72. except pyblockinfo.error as err:
  73. print(err)
  74. return ""
  75. return blkinfo.get("TYPE", None)
  76. def getSize(self):
  77. try:
  78. return pyblockinfo.getSize(self.path)
  79. except pyblockinfo.error as err:
  80. print(err)
  81. return 0
  82. def getAvaiableDevices():
  83. """ Get a all available block devices
  84. """
  85. context = pyudev.Context()
  86. for device in context.list_devices(subsystem="block"):#, DEVTYPE="partition"):
  87. yield DeviceModel(device.device_node, device.device_type)
  88. # https://pyudev.readthedocs.io/en/latest/api/pyudev.html
  89. class DeviceNotifyThread(AbstractThread):
  90. """ Monitor storage device activity in a thread
  91. """
  92. deviceAdded = pyqtSignal(str, str)
  93. deviceRemoved = pyqtSignal(str)
  94. deviceChanged = pyqtSignal(str, str)
  95. def __init__(self):
  96. AbstractThread.__init__(self, flags=THREAD_FLAG_IS_STOPABLE)
  97. self.__run = True
  98. def stop(self):
  99. self.__run = False
  100. def run(self):
  101. context = pyudev.Context()
  102. monitor = pyudev.Monitor.from_netlink(context)
  103. monitor.filter_by("block")
  104. while self.__run is True:
  105. device = monitor.poll(timeout=3)
  106. if device is None:
  107. continue
  108. if device.action == "add":
  109. # TODO artificial delay so the device can init properly, else
  110. # it wont detect the wbfs partition, maybe device.is_initialized is usefull?
  111. time.sleep(1)
  112. self.deviceAdded.emit(device.device_node, device.device_type)
  113. elif device.action == "remove":
  114. self.deviceRemoved.emit(device.device_node)
  115. class DevicesModel(QObject):
  116. """ Main resource for getting storage devices and get updated when one gets
  117. added or removed.
  118. """
  119. deviceAdded = pyqtSignal(str)
  120. deviceRemoved = pyqtSignal(str)
  121. wbfsAdded = pyqtSignal(str)
  122. wbfsRemoved = pyqtSignal(str)
  123. def __init__(self):
  124. QObject.__init__(self)
  125. self.__devices = {}
  126. self.__wbfsDevices = []
  127. self.__notifyThread = DeviceNotifyThread()
  128. @property
  129. def devices(self):
  130. return self.__devices
  131. @property
  132. def wbfsPartitions(self):
  133. for devicePath in self.__wbfsDevices:
  134. if devicePath in self.__devices:
  135. yield self.__devices[devicePath]
  136. def init(self):
  137. self.__devices.clear()
  138. context = pyudev.Context()
  139. for device in context.list_devices(subsystem="block"):#, DEVTYPE="partition"):
  140. devpath = device.device_node
  141. model = DeviceModel(devpath, device.device_type)
  142. self.__devices.update({devpath: model})
  143. self.__rescanWbfsPartitions()
  144. self.__notifyThread.deviceAdded.connect(self.__onDeviceAdded)
  145. self.__notifyThread.deviceRemoved.connect(self.__onDeviceRemoved)
  146. # Start the udev monitor thread
  147. self.__notifyThread.start()
  148. def stop(self):
  149. self.__notifyThread.stop()
  150. self.__notifyThread.wait()
  151. def rescanWbfsPartitions(self):
  152. # Public api (used after format)
  153. # Rescan for WBFS partitions and emit wbfsAdded for new partitions
  154. oldWbfsParts = list(self.__wbfsDevices)
  155. self.__rescanWbfsPartitions()
  156. for devicePath in self.__wbfsDevices:
  157. if devicePath not in self.__devices:
  158. continue
  159. if devicePath not in oldWbfsParts:
  160. self.wbfsAdded.emit(devicePath)
  161. def __rescanWbfsPartitions(self):
  162. # Internal only
  163. self.__wbfsDevices.clear()
  164. try:
  165. self.__wbfsDevices += pywwt.find_wbfs_partitions()
  166. except pywwt.error as err:
  167. print("pywwt.find_wbfs_partitions failed:", err)
  168. def __onDeviceAdded(self, dev, devType):
  169. model = DeviceModel(dev, devType)
  170. self.__devices.update({dev: model})
  171. self.__rescanWbfsPartitions()
  172. self.deviceAdded.emit(dev)
  173. if dev in self.__wbfsDevices:
  174. self.wbfsAdded.emit(dev)
  175. def __onDeviceRemoved(self, devicePath):
  176. device = self.__devices[devicePath]
  177. device.deviceRemoved.emit()
  178. if devicePath in self.__wbfsDevices:
  179. self.__wbfsDevices.remove(devicePath)
  180. self.wbfsRemoved.emit(devicePath)
  181. del self.__devices[devicePath]
  182. #self.__rescanWbfsPartitions() # No need for rescan (only on add)
  183. self.deviceRemoved.emit(devicePath)