progress_updater.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import re
  2. import os
  3. import logging
  4. from utils import TaskRunner
  5. from .manager import (
  6. BuildManager as bm,
  7. BuildState
  8. )
  9. class BuildProgressUpdater:
  10. """
  11. Class for updating the progress of all builds.
  12. This class ensures that the progress of all builds is
  13. updated periodically. It operates in a singleton pattern
  14. to ensure only one instance manages the updates.
  15. """
  16. __singleton = None
  17. def __init__(self):
  18. """
  19. Initialises the BuildProgressUpdater instance.
  20. This uses the BuildManager singleton, so ensure that BuildManager is
  21. initialised before creating a BuildProgressUpdater instance.
  22. Raises:
  23. RuntimeError: If BuildManager is not initialized or
  24. if another instance of BuildProgressUpdater has already
  25. been initialised.
  26. """
  27. if not bm.get_singleton():
  28. raise RuntimeError("BuildManager should be initialised first")
  29. if BuildProgressUpdater.__singleton:
  30. raise RuntimeError("BuildProgressUpdater must be a singleton.")
  31. # Set up a periodic task to update build progress every 3 seconds
  32. # TaskRunner will handle scheduling and running the task.
  33. tasks = (
  34. (self.__update_build_progress_all, 3),
  35. )
  36. self.__runner = TaskRunner(tasks=tasks)
  37. self.logger = logging.getLogger(__name__)
  38. BuildProgressUpdater.__singleton = self
  39. def start(self) -> None:
  40. """
  41. Start BuildProgressUpdater.
  42. """
  43. self.logger.info("Starting BuildProgressUpdater.")
  44. self.__runner.start()
  45. def stop(self) -> None:
  46. """
  47. Stop BuildProgressUpdater.
  48. """
  49. self.logger.info("Stopping BuildProgressUpdater.")
  50. self.__runner.stop()
  51. def __calc_running_build_progress_percent(self, build_id: str) -> int:
  52. """
  53. Calculate the progress percentage of a running build.
  54. This method analyses the build log to determine the current completion
  55. percentage by parsing the build steps from the log file.
  56. Parameters:
  57. build_id (str): The unique ID of the build for which progress is
  58. calculated.
  59. Returns:
  60. int: The calculated build progress percentage (0 to 100).
  61. Raises:
  62. ValueError: If no build information is found for the provided
  63. build ID.
  64. """
  65. build_info = bm.get_singleton().get_build_info(build_id=build_id)
  66. if build_info is None:
  67. raise ValueError(f"No build found with ID {build_id}")
  68. if build_info.progress.state != BuildState.RUNNING:
  69. raise RuntimeError(
  70. "This method should only be called for running builds."
  71. )
  72. # Construct path to the build's log file
  73. log_file_path = bm.get_singleton().get_build_log_path(build_id)
  74. self.logger.debug(f"Opening log file: {log_file_path}")
  75. try:
  76. # Read the log content
  77. with open(log_file_path, encoding='utf-8') as f:
  78. build_log = f.read()
  79. except FileNotFoundError:
  80. self.logger.error(
  81. f"Log file not found for RUNNING build with ID: {build_id}"
  82. )
  83. return build_info.progress.percent
  84. # Regular expression to extract the build progress steps
  85. compiled_regex = re.compile(r'(\[\D*(\d+)\D*\/\D*(\d+)\D*\])')
  86. self.logger.debug(f"Regex pattern: {compiled_regex}")
  87. all_matches = compiled_regex.findall(build_log)
  88. self.logger.debug(f"Log matches: {all_matches}")
  89. # If no matches are found, return a default progress value of 0
  90. if len(all_matches) < 1:
  91. return 0
  92. completed_steps, total_steps = all_matches[-1][1:]
  93. self.logger.debug(
  94. f"Completed steps: {completed_steps},"
  95. f"Total steps: {total_steps}"
  96. )
  97. # Handle initial compilation/linking steps (minor weight)
  98. if int(total_steps) < 20:
  99. return 1
  100. # Handle building the OS phase (4% weight)
  101. if int(total_steps) < 200:
  102. return (int(completed_steps) * 4 // int(total_steps)) + 1
  103. # Major build phase (95% weight)
  104. return (int(completed_steps) * 95 // int(total_steps)) + 5
  105. def __refresh_running_build_state(self, build_id: str) -> BuildState:
  106. """
  107. Refresh the state of a running build.
  108. This method analyses the build log to determine the build has
  109. concluded. If yes, it detects the success of a build by finding
  110. the success message in the log.
  111. Parameters:
  112. build_id (str): The unique ID of the build for which progress is
  113. calculated.
  114. Returns:
  115. BuildSate: The current build state based on the log.
  116. Raises:
  117. ValueError: If no build information is found for the provided
  118. build ID.
  119. """
  120. build_info = bm.get_singleton().get_build_info(build_id=build_id)
  121. if build_info is None:
  122. raise ValueError(f"No build found with ID {build_id}")
  123. if build_info.progress.state != BuildState.RUNNING:
  124. raise RuntimeError(
  125. "This method should only be called for running builds."
  126. )
  127. # Builder ships the archive post completion
  128. # This is irrespective of SUCCESS or FAILURE
  129. if not os.path.exists(
  130. bm.get_singleton().get_build_archive_path(build_id)
  131. ):
  132. return BuildState.RUNNING
  133. log_file_path = bm.get_singleton().get_build_log_path(build_id)
  134. try:
  135. # Read the log content
  136. with open(log_file_path, encoding='utf-8') as f:
  137. build_log = f.read()
  138. except FileNotFoundError:
  139. self.logger.error(
  140. f"Log file not found for RUNNING build with ID: {build_id}"
  141. )
  142. return BuildState.ERROR
  143. # Build has finished, check if it succeeded or failed
  144. flash_summary_pos = build_log.find("Total Flash Used")
  145. if flash_summary_pos == -1:
  146. return BuildState.FAILURE
  147. else:
  148. return BuildState.SUCCESS
  149. def __update_build_percent(self, build_id: str) -> None:
  150. """
  151. Update the progress percentage of a given build.
  152. """
  153. build_info = bm.get_singleton().get_build_info(build_id=build_id)
  154. if build_info is None:
  155. raise ValueError(f"No build found with ID {build_id}")
  156. current_state = build_info.progress.state
  157. current_percent = build_info.progress.percent
  158. new_percent = current_percent
  159. self.logger.debug(
  160. f"Build id: {build_id}, "
  161. f"Current state: {current_state}, "
  162. f"Current percentage: {current_percent}, "
  163. )
  164. if current_state == BuildState.PENDING:
  165. # Keep existing percentage
  166. pass
  167. elif current_state == BuildState.RUNNING:
  168. new_percent = self.__calc_running_build_progress_percent(build_id)
  169. elif current_state == BuildState.SUCCESS:
  170. new_percent = 100
  171. elif current_state == BuildState.FAILURE:
  172. # Keep existing percentage
  173. pass
  174. elif current_state == BuildState.ERROR:
  175. # Keep existing percentage
  176. pass
  177. else:
  178. raise Exception("Unhandled BuildState.")
  179. self.logger.debug(
  180. f"Build id: {build_id}, "
  181. f"New percentage: {new_percent}, "
  182. )
  183. if new_percent != current_percent:
  184. bm.get_singleton().update_build_progress_percent(
  185. build_id=build_id,
  186. percent=new_percent
  187. )
  188. def __update_build_state(self, build_id: str) -> None:
  189. """
  190. Update the state of a given build.
  191. """
  192. build_info = bm.get_singleton().get_build_info(build_id=build_id)
  193. if build_info is None:
  194. raise ValueError(f"No build found with ID {build_id}")
  195. current_state = build_info.progress.state
  196. new_state = current_state
  197. self.logger.debug(
  198. f"Build id: {build_id}, "
  199. f"Current state: {current_state.name}, "
  200. )
  201. log_file_path = bm.get_singleton().get_build_log_path(build_id)
  202. if current_state == BuildState.PENDING:
  203. # Builder creates log file when it starts
  204. # running a build
  205. if os.path.exists(log_file_path):
  206. new_state = BuildState.RUNNING
  207. elif current_state == BuildState.RUNNING:
  208. new_state = self.__refresh_running_build_state(build_id)
  209. elif current_state == BuildState.SUCCESS:
  210. # SUCCESS is a conclusive state
  211. pass
  212. elif current_state == BuildState.FAILURE:
  213. # FAILURE is a conclusive state
  214. pass
  215. elif current_state == BuildState.ERROR:
  216. # ERROR is a conclusive state
  217. pass
  218. else:
  219. raise Exception("Unhandled BuildState.")
  220. self.logger.debug(
  221. f"Build id: {build_id}, "
  222. f"New state: {new_state.name}, "
  223. )
  224. if current_state != new_state:
  225. bm.get_singleton().update_build_progress_state(
  226. build_id=build_id,
  227. new_state=new_state,
  228. )
  229. def __update_build_progress_all(self) -> None:
  230. """
  231. Update progress for all builds.
  232. This method will iterate through all builds, calculate their
  233. progress, and update the build manager with the latest progress state
  234. and percentage.
  235. """
  236. for build_id in bm.get_singleton().get_all_build_ids():
  237. self.__update_build_state(build_id)
  238. self.__update_build_percent(build_id)
  239. @staticmethod
  240. def get_singleton() -> "BuildProgressUpdater":
  241. """
  242. Get the singleton instance of BuildProgressUpdater.
  243. Returns:
  244. BuildProgressUpdater: The singleton instance of this class.
  245. """
  246. return BuildProgressUpdater.__singleton