simstack 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. simstack/__init__.py +0 -0
  2. simstack/_version.py +24 -0
  3. simstack/core/__init__.py +0 -0
  4. simstack/core/artifacts.py +406 -0
  5. simstack/core/asnyc_helper.py +14 -0
  6. simstack/core/config_file.py +60 -0
  7. simstack/core/context.py +283 -0
  8. simstack/core/definitions.py +22 -0
  9. simstack/core/engine.py +115 -0
  10. simstack/core/find_simstack_modules.py +41 -0
  11. simstack/core/hash.py +179 -0
  12. simstack/core/node.py +1137 -0
  13. simstack/core/node_claim.py +31 -0
  14. simstack/core/node_runner.py +362 -0
  15. simstack/core/process_results.py +102 -0
  16. simstack/core/recompute_artifacts.py +117 -0
  17. simstack/core/reset_db.py +12 -0
  18. simstack/core/resource_assignment.py +222 -0
  19. simstack/core/resources.py +90 -0
  20. simstack/core/route_table.py +24 -0
  21. simstack/core/run_docker.py +109 -0
  22. simstack/core/run_node.py +71 -0
  23. simstack/core/runner.py +169 -0
  24. simstack/core/services/__init__.py +11 -0
  25. simstack/core/services/base_service.py +191 -0
  26. simstack/core/services/file_transfer_service.py +117 -0
  27. simstack/core/services/git_restart_service.py +12 -0
  28. simstack/core/services/git_uv_update_service.py +105 -0
  29. simstack/core/services/node_execution_service.py +182 -0
  30. simstack/core/services/resource_branch_monitor_service.py +71 -0
  31. simstack/core/services/runner_cleanup_service.py +149 -0
  32. simstack/core/services/runner_manager.py +163 -0
  33. simstack/core/services/runner_status_service.py +10 -0
  34. simstack/core/services/slurm_status_service.py +67 -0
  35. simstack/core/services/stop_check_service.py +27 -0
  36. simstack/core/services/timeout_restart_service.py +26 -0
  37. simstack/core/simstack_result.py +39 -0
  38. simstack/core/submit_node.py +217 -0
  39. simstack/core/task_id.py +22 -0
  40. simstack/methods/__init__.py +0 -0
  41. simstack/methods/archive_files.py +292 -0
  42. simstack/methods/fire_and_forget_runner.py +113 -0
  43. simstack/methods/generate_test.py +125 -0
  44. simstack/methods/get_file.py +26 -0
  45. simstack/methods/mass_runner.py +154 -0
  46. simstack/methods/multiple_executor.py +307 -0
  47. simstack/methods/switch_git.py +16 -0
  48. simstack/methods/upload_helpers.py +81 -0
  49. simstack/models/__init__.py +74 -0
  50. simstack/models/array_list.py +21 -0
  51. simstack/models/array_storage.py +83 -0
  52. simstack/models/artifact_models.py +127 -0
  53. simstack/models/base_lists.py +51 -0
  54. simstack/models/base_types.py +209 -0
  55. simstack/models/charts_artifact.py +690 -0
  56. simstack/models/dataset.py +451 -0
  57. simstack/models/dataset_metadata.py +390 -0
  58. simstack/models/datasettuple.py +587 -0
  59. simstack/models/file_instance.py +150 -0
  60. simstack/models/file_list.py +45 -0
  61. simstack/models/files.py +424 -0
  62. simstack/models/fire_and_forget_result.py +19 -0
  63. simstack/models/images2d.py +113 -0
  64. simstack/models/log_entry_model.py +37 -0
  65. simstack/models/models.py +52 -0
  66. simstack/models/named_data_reference.py +36 -0
  67. simstack/models/node_registry.py +96 -0
  68. simstack/models/pandas_model.py +239 -0
  69. simstack/models/parameters.py +402 -0
  70. simstack/models/pickle_models.py +300 -0
  71. simstack/models/project.py +13 -0
  72. simstack/models/resource_assignment.py +170 -0
  73. simstack/models/resource_definition.py +125 -0
  74. simstack/models/runner_model.py +39 -0
  75. simstack/models/simple_table.py +79 -0
  76. simstack/models/simstack_model.py +97 -0
  77. simstack/models/slurm_info.py +17 -0
  78. simstack/models/table_artifact.py +87 -0
  79. simstack/models/tag.py +12 -0
  80. simstack/models/unused_test_file.py +5 -0
  81. simstack/simstack_marker.com +1 -0
  82. simstack/static/docs/.buildinfo +4 -0
  83. simstack/static/docs/_images/GUI Concept.webp +0 -0
  84. simstack/static/docs/_modules/index.html +434 -0
  85. simstack/static/docs/_modules/simstack/core/artifacts.html +743 -0
  86. simstack/static/docs/_modules/simstack/core/asnyc_helper.html +330 -0
  87. simstack/static/docs/_modules/simstack/core/config_file.html +379 -0
  88. simstack/static/docs/_modules/simstack/core/definitions.html +341 -0
  89. simstack/static/docs/_modules/simstack/core/engine.html +437 -0
  90. simstack/static/docs/_modules/simstack/core/find_simstack_modules.html +360 -0
  91. simstack/static/docs/_modules/simstack/core/hash.html +528 -0
  92. simstack/static/docs/_modules/simstack/core/node.html +1392 -0
  93. simstack/static/docs/_modules/simstack/core/node_claim.html +347 -0
  94. simstack/static/docs/_modules/simstack/core/node_runner.html +711 -0
  95. simstack/static/docs/_modules/simstack/core/process_results.html +418 -0
  96. simstack/static/docs/_modules/simstack/core/recompute_artifacts.html +433 -0
  97. simstack/static/docs/_modules/simstack/core/reset_db.html +328 -0
  98. simstack/static/docs/_modules/simstack/core/resource_assignment.html +553 -0
  99. simstack/static/docs/_modules/simstack/core/resources.html +424 -0
  100. simstack/static/docs/_modules/simstack/core/route_table.html +349 -0
  101. simstack/static/docs/_modules/simstack/core/run_docker.html +425 -0
  102. simstack/static/docs/_modules/simstack/core/run_node.html +390 -0
  103. simstack/static/docs/_modules/simstack/core/runner.html +491 -0
  104. simstack/static/docs/_modules/simstack/core/services/base_service.html +522 -0
  105. simstack/static/docs/_modules/simstack/core/services/file_transfer_service.html +436 -0
  106. simstack/static/docs/_modules/simstack/core/services/git_restart_service.html +331 -0
  107. simstack/static/docs/_modules/simstack/core/services/git_uv_update_service.html +427 -0
  108. simstack/static/docs/_modules/simstack/core/services/node_execution_service.html +507 -0
  109. simstack/static/docs/_modules/simstack/core/services/resource_branch_monitor_service.html +390 -0
  110. simstack/static/docs/_modules/simstack/core/services/runner_cleanup_service.html +468 -0
  111. simstack/static/docs/_modules/simstack/core/services/runner_manager.html +485 -0
  112. simstack/static/docs/_modules/simstack/core/services/runner_status_service.html +329 -0
  113. simstack/static/docs/_modules/simstack/core/services/slurm_status_service.html +386 -0
  114. simstack/static/docs/_modules/simstack/core/services/stop_check_service.html +346 -0
  115. simstack/static/docs/_modules/simstack/core/services/timeout_restart_service.html +345 -0
  116. simstack/static/docs/_modules/simstack/core/simstack_result.html +355 -0
  117. simstack/static/docs/_modules/simstack/core/submit_node.html +536 -0
  118. simstack/static/docs/_modules/simstack/core/task_id.html +344 -0
  119. simstack/static/docs/_modules/simstack/methods/archive_files.html +598 -0
  120. simstack/static/docs/_modules/simstack/methods/fire_and_forget_runner.html +432 -0
  121. simstack/static/docs/_modules/simstack/methods/generate_test.html +443 -0
  122. simstack/static/docs/_modules/simstack/methods/get_file.html +342 -0
  123. simstack/static/docs/_modules/simstack/methods/mass_runner.html +476 -0
  124. simstack/static/docs/_modules/simstack/methods/multiple_executor.html +674 -0
  125. simstack/static/docs/_modules/simstack/methods/switch_git.html +332 -0
  126. simstack/static/docs/_modules/simstack/methods/upload_helpers.html +403 -0
  127. simstack/static/docs/_modules/simstack/models/array_list.html +337 -0
  128. simstack/static/docs/_modules/simstack/models/array_storage.html +417 -0
  129. simstack/static/docs/_modules/simstack/models/base_lists.html +374 -0
  130. simstack/static/docs/_modules/simstack/models/base_types.html +588 -0
  131. simstack/static/docs/_modules/simstack/models/charts_artifact.html +1035 -0
  132. simstack/static/docs/_modules/simstack/models/dataset.html +866 -0
  133. simstack/static/docs/_modules/simstack/models/dataset_metadata.html +711 -0
  134. simstack/static/docs/_modules/simstack/models/datasettuple.html +1005 -0
  135. simstack/static/docs/_modules/simstack/models/file_instance.html +472 -0
  136. simstack/static/docs/_modules/simstack/models/file_list.html +367 -0
  137. simstack/static/docs/_modules/simstack/models/files.html +773 -0
  138. simstack/static/docs/_modules/simstack/models/fire_and_forget_result.html +335 -0
  139. simstack/static/docs/_modules/simstack/models/images2d.html +447 -0
  140. simstack/static/docs/_modules/simstack/models/log_entry_model.html +356 -0
  141. simstack/static/docs/_modules/simstack/models/models.html +374 -0
  142. simstack/static/docs/_modules/simstack/models/named_data_reference.html +355 -0
  143. simstack/static/docs/_modules/simstack/models/node_registry.html +415 -0
  144. simstack/static/docs/_modules/simstack/models/pandas_model.html +570 -0
  145. simstack/static/docs/_modules/simstack/models/parameters.html +739 -0
  146. simstack/static/docs/_modules/simstack/models/pickle_models.html +631 -0
  147. simstack/static/docs/_modules/simstack/models/project.html +329 -0
  148. simstack/static/docs/_modules/simstack/models/resource_assignment.html +501 -0
  149. simstack/static/docs/_modules/simstack/models/resource_definition.html +477 -0
  150. simstack/static/docs/_modules/simstack/models/runner_model.html +361 -0
  151. simstack/static/docs/_modules/simstack/models/simple_table.html +413 -0
  152. simstack/static/docs/_modules/simstack/models/simstack_model.html +416 -0
  153. simstack/static/docs/_modules/simstack/models/slurm_info.html +333 -0
  154. simstack/static/docs/_modules/simstack/models/table_artifact.html +406 -0
  155. simstack/static/docs/_modules/simstack/models/tag.html +328 -0
  156. simstack/static/docs/_modules/simstack/tables/model_table.html +519 -0
  157. simstack/static/docs/_modules/simstack/tables/node_children.html +538 -0
  158. simstack/static/docs/_modules/simstack/tables/node_table.html +854 -0
  159. simstack/static/docs/_modules/simstack/tables/table_builder.html +642 -0
  160. simstack/static/docs/_modules/simstack/util/async_zip_utils.html +337 -0
  161. simstack/static/docs/_modules/simstack/util/b64mixin.html +339 -0
  162. simstack/static/docs/_modules/simstack/util/cleaned_json_schema.html +336 -0
  163. simstack/static/docs/_modules/simstack/util/config_reader.html +498 -0
  164. simstack/static/docs/_modules/simstack/util/custom_model_dump.html +391 -0
  165. simstack/static/docs/_modules/simstack/util/database_information.html +488 -0
  166. simstack/static/docs/_modules/simstack/util/db.html +1392 -0
  167. simstack/static/docs/_modules/simstack/util/db_logger.html +448 -0
  168. simstack/static/docs/_modules/simstack/util/default_from_dict.html +376 -0
  169. simstack/static/docs/_modules/simstack/util/directory_iterator.html +553 -0
  170. simstack/static/docs/_modules/simstack/util/docstring_parser.html +526 -0
  171. simstack/static/docs/_modules/simstack/util/file_hashing.html +394 -0
  172. simstack/static/docs/_modules/simstack/util/file_transfer_client.html +791 -0
  173. simstack/static/docs/_modules/simstack/util/files.html +733 -0
  174. simstack/static/docs/_modules/simstack/util/find_python_files.html +349 -0
  175. simstack/static/docs/_modules/simstack/util/generate_ui_schema.html +396 -0
  176. simstack/static/docs/_modules/simstack/util/generic_list_mixin.html +487 -0
  177. simstack/static/docs/_modules/simstack/util/get_module_path.html +336 -0
  178. simstack/static/docs/_modules/simstack/util/git_pull.html +381 -0
  179. simstack/static/docs/_modules/simstack/util/git_repository_status.html +411 -0
  180. simstack/static/docs/_modules/simstack/util/import_module.html +377 -0
  181. simstack/static/docs/_modules/simstack/util/importer.html +637 -0
  182. simstack/static/docs/_modules/simstack/util/init_data_source.html +357 -0
  183. simstack/static/docs/_modules/simstack/util/make_graph.html +423 -0
  184. simstack/static/docs/_modules/simstack/util/make_table.html +1171 -0
  185. simstack/static/docs/_modules/simstack/util/mappings.html +378 -0
  186. simstack/static/docs/_modules/simstack/util/minimal_route_finder.html +388 -0
  187. simstack/static/docs/_modules/simstack/util/mock_db.html +472 -0
  188. simstack/static/docs/_modules/simstack/util/module_path_checker.html +354 -0
  189. simstack/static/docs/_modules/simstack/util/object_list_mixin.html +649 -0
  190. simstack/static/docs/_modules/simstack/util/path_manager.html +566 -0
  191. simstack/static/docs/_modules/simstack/util/project_root_finder.html +357 -0
  192. simstack/static/docs/_modules/simstack/util/queue_watcher.html +593 -0
  193. simstack/static/docs/_modules/simstack/util/resource_config.html +532 -0
  194. simstack/static/docs/_modules/simstack/util/route_finder.html +390 -0
  195. simstack/static/docs/_modules/simstack/util/runner_util.html +487 -0
  196. simstack/static/docs/_modules/simstack/util/runner_utils.html +468 -0
  197. simstack/static/docs/_modules/simstack/util/safe_code_executor.html +510 -0
  198. simstack/static/docs/_modules/simstack/util/sample_config.html +414 -0
  199. simstack/static/docs/_modules/simstack/util/sensitive_content_filter.html +364 -0
  200. simstack/static/docs/_modules/simstack/util/setup_logging.html +365 -0
  201. simstack/static/docs/_modules/simstack/util/submit_to_watchdog.html +425 -0
  202. simstack/static/docs/_modules/simstack/util/toml_reader.html +482 -0
  203. simstack/static/docs/_modules/simstack/util/transform_file_name.html +357 -0
  204. simstack/static/docs/_modules/simstack/util/ui_tools.html +541 -0
  205. simstack/static/docs/_sources/architecture.rst.txt +25 -0
  206. simstack/static/docs/_sources/array_lists.rst.txt +60 -0
  207. simstack/static/docs/_sources/ci_pipeline.rst.txt +34 -0
  208. simstack/static/docs/_sources/data.rst.txt +175 -0
  209. simstack/static/docs/_sources/database_migrations.rst.txt +310 -0
  210. simstack/static/docs/_sources/datasets.rst.txt +93 -0
  211. simstack/static/docs/_sources/developer_guide.rst.txt +25 -0
  212. simstack/static/docs/_sources/docker_nodes.rst.txt +149 -0
  213. simstack/static/docs/_sources/files.rst.txt +159 -0
  214. simstack/static/docs/_sources/fire_and_forget_runner.rst.txt +67 -0
  215. simstack/static/docs/_sources/index.rst.txt +25 -0
  216. simstack/static/docs/_sources/installation.rst.txt +213 -0
  217. simstack/static/docs/_sources/lists.rst.txt +111 -0
  218. simstack/static/docs/_sources/mass_runner.rst.txt +91 -0
  219. simstack/static/docs/_sources/misc.rst.txt +106 -0
  220. simstack/static/docs/_sources/overview.rst.txt +91 -0
  221. simstack/static/docs/_sources/recovery.rst.txt +38 -0
  222. simstack/static/docs/_sources/resource_config.rst.txt +141 -0
  223. simstack/static/docs/_sources/simstack/modules/modules.rst.txt +7 -0
  224. simstack/static/docs/_sources/simstack/modules/simstack.core.rst.txt +205 -0
  225. simstack/static/docs/_sources/simstack/modules/simstack.core.services.rst.txt +109 -0
  226. simstack/static/docs/_sources/simstack/modules/simstack.methods.rst.txt +77 -0
  227. simstack/static/docs/_sources/simstack/modules/simstack.models.rst.txt +261 -0
  228. simstack/static/docs/_sources/simstack/modules/simstack.rst.txt +22 -0
  229. simstack/static/docs/_sources/simstack/modules/simstack.tables.rst.txt +45 -0
  230. simstack/static/docs/_sources/simstack/modules/simstack.util.rst.txt +381 -0
  231. simstack/static/docs/_sources/submit.rst.txt +141 -0
  232. simstack/static/docs/_sources/tables.rst.txt +50 -0
  233. simstack/static/docs/_sources/testing_complex_nodes.rst.txt +92 -0
  234. simstack/static/docs/_sources/ui_documentation.rst.txt +272 -0
  235. simstack/static/docs/_sources/workflows.rst.txt +446 -0
  236. simstack/static/docs/_static/base-stemmer.js +476 -0
  237. simstack/static/docs/_static/basic.css +906 -0
  238. simstack/static/docs/_static/cobalt2-theme.css +1 -0
  239. simstack/static/docs/_static/debug.css +69 -0
  240. simstack/static/docs/_static/docs-build-badge.css +51 -0
  241. simstack/static/docs/_static/docs-build-badge.js +51 -0
  242. simstack/static/docs/_static/doctools.js +150 -0
  243. simstack/static/docs/_static/documentation_options.js +13 -0
  244. simstack/static/docs/_static/english-stemmer.js +1066 -0
  245. simstack/static/docs/_static/file.png +0 -0
  246. simstack/static/docs/_static/language_data.js +13 -0
  247. simstack/static/docs/_static/minus.png +0 -0
  248. simstack/static/docs/_static/plus.png +0 -0
  249. simstack/static/docs/_static/pygments.css +250 -0
  250. simstack/static/docs/_static/scripts/furo-extensions.js +0 -0
  251. simstack/static/docs/_static/scripts/furo.js +3 -0
  252. simstack/static/docs/_static/scripts/furo.js.LICENSE.txt +7 -0
  253. simstack/static/docs/_static/scripts/furo.js.map +1 -0
  254. simstack/static/docs/_static/searchtools.js +693 -0
  255. simstack/static/docs/_static/skeleton.css +296 -0
  256. simstack/static/docs/_static/sphinx_highlight.js +159 -0
  257. simstack/static/docs/_static/styles/furo-extensions.css +2 -0
  258. simstack/static/docs/_static/styles/furo-extensions.css.map +1 -0
  259. simstack/static/docs/_static/styles/furo.css +2 -0
  260. simstack/static/docs/_static/styles/furo.css.map +1 -0
  261. simstack/static/docs/architecture.html +340 -0
  262. simstack/static/docs/array_lists.html +678 -0
  263. simstack/static/docs/ci_pipeline.html +385 -0
  264. simstack/static/docs/data.html +545 -0
  265. simstack/static/docs/database_migrations.html +652 -0
  266. simstack/static/docs/datasets.html +1268 -0
  267. simstack/static/docs/developer_guide.html +359 -0
  268. simstack/static/docs/docker_nodes.html +503 -0
  269. simstack/static/docs/files.html +718 -0
  270. simstack/static/docs/fire_and_forget_runner.html +641 -0
  271. simstack/static/docs/genindex.html +6390 -0
  272. simstack/static/docs/index.html +516 -0
  273. simstack/static/docs/installation.html +553 -0
  274. simstack/static/docs/lists.html +478 -0
  275. simstack/static/docs/mass_runner.html +481 -0
  276. simstack/static/docs/misc.html +454 -0
  277. simstack/static/docs/objects.inv +0 -0
  278. simstack/static/docs/overview.html +469 -0
  279. simstack/static/docs/py-modindex.html +1111 -0
  280. simstack/static/docs/recovery.html +371 -0
  281. simstack/static/docs/resource_config.html +590 -0
  282. simstack/static/docs/search.html +327 -0
  283. simstack/static/docs/searchindex.js +1 -0
  284. simstack/static/docs/simstack/modules/modules.html +489 -0
  285. simstack/static/docs/simstack/modules/simstack.core.html +1883 -0
  286. simstack/static/docs/simstack/modules/simstack.core.services.html +698 -0
  287. simstack/static/docs/simstack/modules/simstack.html +3060 -0
  288. simstack/static/docs/simstack/modules/simstack.methods.html +1150 -0
  289. simstack/static/docs/simstack/modules/simstack.models.html +13113 -0
  290. simstack/static/docs/simstack/modules/simstack.tables.html +635 -0
  291. simstack/static/docs/simstack/modules/simstack.util.html +3320 -0
  292. simstack/static/docs/submit.html +487 -0
  293. simstack/static/docs/tables.html +539 -0
  294. simstack/static/docs/testing_complex_nodes.html +458 -0
  295. simstack/static/docs/ui_documentation.html +788 -0
  296. simstack/static/docs/workflows.html +766 -0
  297. simstack/tables/__init__.py +0 -0
  298. simstack/tables/model_table.py +191 -0
  299. simstack/tables/node_children.py +219 -0
  300. simstack/tables/node_table.py +517 -0
  301. simstack/tables/table_builder.py +314 -0
  302. simstack/util/__init__.py +0 -0
  303. simstack/util/async_zip_utils.py +18 -0
  304. simstack/util/b64mixin.py +23 -0
  305. simstack/util/cleaned_json_schema.py +20 -0
  306. simstack/util/config_reader.py +179 -0
  307. simstack/util/custom_model_dump.py +75 -0
  308. simstack/util/database_information.py +160 -0
  309. simstack/util/db.py +1013 -0
  310. simstack/util/db_logger.py +117 -0
  311. simstack/util/default_from_dict.py +57 -0
  312. simstack/util/directory_iterator.py +216 -0
  313. simstack/util/docstring_parser.py +186 -0
  314. simstack/util/file_hashing.py +72 -0
  315. simstack/util/file_transfer_client.py +427 -0
  316. simstack/util/files.py +396 -0
  317. simstack/util/find_python_files.py +33 -0
  318. simstack/util/generate_ui_schema.py +80 -0
  319. simstack/util/generic_list_mixin.py +120 -0
  320. simstack/util/geometry_utils.py +118 -0
  321. simstack/util/get_module_path.py +20 -0
  322. simstack/util/git_pull.py +65 -0
  323. simstack/util/git_repository_status.py +95 -0
  324. simstack/util/import_module.py +63 -0
  325. simstack/util/importer.py +312 -0
  326. simstack/util/init_data_source.py +41 -0
  327. simstack/util/make_graph.py +107 -0
  328. simstack/util/make_table.py +837 -0
  329. simstack/util/mappings.py +41 -0
  330. simstack/util/minimal_route_finder.py +72 -0
  331. simstack/util/mock_db.py +144 -0
  332. simstack/util/module_path_checker.py +38 -0
  333. simstack/util/object_list_mixin.py +282 -0
  334. simstack/util/path_manager.py +223 -0
  335. simstack/util/project_root_finder.py +41 -0
  336. simstack/util/queue_watcher.py +262 -0
  337. simstack/util/resource_config.py +198 -0
  338. simstack/util/route_finder.py +74 -0
  339. simstack/util/runner_util.py +166 -0
  340. simstack/util/runner_utils.py +143 -0
  341. simstack/util/safe_code_executor.py +194 -0
  342. simstack/util/sample_config.py +98 -0
  343. simstack/util/sensitive_content_filter.py +48 -0
  344. simstack/util/setup_logging.py +49 -0
  345. simstack/util/submit_to_watchdog.py +106 -0
  346. simstack/util/toml_reader.py +142 -0
  347. simstack/util/transform_file_name.py +41 -0
  348. simstack/util/ui_tools.py +213 -0
  349. simstack-2.0.1.dist-info/METADATA +454 -0
  350. simstack-2.0.1.dist-info/RECORD +354 -0
  351. simstack-2.0.1.dist-info/WHEEL +5 -0
  352. simstack-2.0.1.dist-info/entry_points.txt +12 -0
  353. simstack-2.0.1.dist-info/licenses/LICENSE +21 -0
  354. simstack-2.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,191 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import socket
5
+ import sys
6
+ import platform
7
+ import subprocess
8
+ from abc import ABC, abstractmethod
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from odmantic import ObjectId
12
+
13
+ from simstack.core.context import context
14
+ from simstack.models.parameters import Resource
15
+ from simstack.models.runner_model import RunnerEvent, RunnerType, RunnerEventEnum
16
+ from simstack.util.runner_utils import make_git_status_list
17
+
18
+ logger = logging.getLogger("NodeRunner")
19
+
20
+ class BaseService(ABC):
21
+ """A managed periodic service that can be stopped gracefully"""
22
+
23
+ def __init__(self, name: str, resource: Resource, interval: int, shutdown_event: asyncio.Event = None):
24
+ self._name = name
25
+ self._resource = resource
26
+ self._interval: int = interval
27
+ self._stop_event = asyncio.Event()
28
+ self._shutdown_event = shutdown_event
29
+ self._task = None
30
+ self._consecutive_failures = 0
31
+ self._log_traceback_on_failure = True
32
+
33
+ # Common identity attributes
34
+ self._pid = os.getpid()
35
+ self._hostname = socket.gethostname()
36
+ self._username = os.environ.get("USER", os.environ.get("USERNAME", "unknown"))
37
+ self._time_started = datetime.now()
38
+
39
+ def _get_uptime_string(self) -> str:
40
+ time_diff = datetime.now() - self._time_started
41
+ total_seconds = int(time_diff.total_seconds())
42
+ days, remainder = divmod(total_seconds, 86400)
43
+ hours, remainder = divmod(remainder, 3600)
44
+ minutes, seconds = divmod(remainder, 60)
45
+ return f"{days}d {hours}h {minutes}m {seconds}s"
46
+
47
+ async def write_node_event(self, event: RunnerEventEnum, node_id: ObjectId, message: str = None):
48
+ runner_event = RunnerEvent(
49
+ runner_type=RunnerType.NODE_RUNNER,
50
+ event=event,
51
+ pid=self._pid,
52
+ hostname=self._hostname,
53
+ user=self._username,
54
+ resource=self._resource,
55
+ node_id=node_id,
56
+ message=message,
57
+ )
58
+ await context.db.save(runner_event)
59
+
60
+ async def write_resource_event(self, event: RunnerEventEnum, message: str = None):
61
+ git_list = make_git_status_list()
62
+ if event == RunnerEventEnum.ALIVE:
63
+ uptime = self._get_uptime_string()
64
+ message = f"Uptime: {uptime}"
65
+
66
+ runner_event = await context.db.find_one(
67
+ RunnerEvent,
68
+ (RunnerEvent.runner_type == RunnerType.RESOURCE_RUNNER)
69
+ & (RunnerEvent.resource == self._resource)
70
+ & (RunnerEvent.event == RunnerEventEnum.ALIVE)
71
+ & (RunnerEvent.pid == self._pid),
72
+ )
73
+ if runner_event:
74
+ runner_event.message = message
75
+ runner_event.timestamp = datetime.now()
76
+ runner_event.git_status = git_list
77
+ await context.db.save(runner_event)
78
+ return
79
+
80
+ runner_event = RunnerEvent(
81
+ runner_type=RunnerType.RESOURCE_RUNNER,
82
+ pid=self._pid,
83
+ hostname=self._hostname,
84
+ user=self._username,
85
+ timestamp=datetime.now(),
86
+ git_status=git_list,
87
+ event=event,
88
+ resource=self._resource,
89
+ message=message,
90
+ )
91
+ await context.db.save(runner_event)
92
+
93
+ async def _run_loop(self):
94
+ """Internal loop that respects the stop event"""
95
+ logger.info(f"Service {self._name} started.")
96
+ while not self._stop_event.is_set():
97
+ start_time = asyncio.get_event_loop().time()
98
+ try:
99
+ await self.execute()
100
+ self._consecutive_failures = 0
101
+ except Exception as e:
102
+ self._consecutive_failures += 1
103
+ msg = f"Error in service {self._name} (failure {self._consecutive_failures}/3): {e}"
104
+ if self._log_traceback_on_failure:
105
+ logger.exception(msg)
106
+ else:
107
+ logger.error(msg)
108
+
109
+ if self._consecutive_failures >= 3:
110
+ logger.error(f"Service {self._name} failed 3 times in a row. Shutting down.")
111
+ self._stop_event.set()
112
+ if self._shutdown_event:
113
+ self._shutdown_event.set()
114
+
115
+ # Calculate wait time to maintain interval regardless of execution duration
116
+ elapsed = asyncio.get_event_loop().time() - start_time
117
+ wait_time = max(0, int(self._interval - elapsed))
118
+
119
+ try:
120
+ # Wait for interval OR until stop event is set
121
+ await asyncio.wait_for(self._stop_event.wait(), timeout=wait_time)
122
+ except asyncio.TimeoutError:
123
+ # Normal path: the interval passed, loop again
124
+ pass
125
+
126
+ logger.info(f"Service {self._name} stopped.")
127
+
128
+ @abstractmethod
129
+ async def execute(self):
130
+ raise NotImplementedError("Services must implement execute()")
131
+
132
+ def start(self):
133
+ if self._task is None or self._task.done():
134
+ self._stop_event.clear()
135
+ self._task = asyncio.create_task(self._run_loop())
136
+ return self._task
137
+
138
+ async def stop(self):
139
+ self._stop_event.set()
140
+ if self._task:
141
+ await self._task
142
+
143
+
144
+ class RestartService(BaseService, ABC):
145
+ """
146
+ Intermediate abstract class for services that can trigger a runner restart.
147
+ """
148
+
149
+ def __init__(self, name: str, resource: Resource, interval: int):
150
+ super().__init__(name, resource, interval)
151
+ self._pid_file = context.config.workdir / "runner.pid"
152
+
153
+ async def trigger_restart(self):
154
+ """Spawns an independent process to kill current PID and start new one"""
155
+ current_pid = os.getpid()
156
+
157
+ # Write current PID to file so the restarter knows who to kill
158
+ self._pid_file.write_text(str(current_pid))
159
+
160
+ # Command to restart depends on OS
161
+ # We use sys.executable to ensure we use the same Python interpreter
162
+ # We'll call the runner module again
163
+ # IMPORTANT: We need the path to the original runner.py script.
164
+ # Since we are now in services/base_service.py, we need to go up two levels.
165
+ # But actually Path(__file__).resolve() in the original runner.py was used.
166
+ # We should probably pass the runner script path or rely on how it was invoked.
167
+ # In runner.py it was: script_path = Path(__file__).resolve()
168
+
169
+ # Let's try to find runner.py relative to this file.
170
+ script_path = (Path(__file__).parent.parent / "runner.py").resolve()
171
+
172
+ log_file = context.config.workdir / "runner.out"
173
+
174
+ args = [sys.executable, str(script_path)] + sys.argv[1:]
175
+
176
+ # We need a small helper script or a one-liner that:
177
+ # 1. Waits for this process to die (to avoid port/resource conflicts)
178
+ # 2. Starts the new one
179
+ if platform.system() == "Windows":
180
+ # Windows 'start' command handles detachment well
181
+ # Use ping for a 2-second delay as 'timeout' fails in non-interactive shells
182
+ cmd = f"taskkill /F /PID {current_pid} && ping 127.0.0.1 -n 3 > nul && {' '.join(args)} >> \"{log_file}\" 2>&1"
183
+ subprocess.Popen(cmd, shell=True, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
184
+ else:
185
+ # Linux: use a subshell that nohup/disowns
186
+ # Kill, wait, and start
187
+ cmd = f"kill -9 {current_pid} && sleep 2 && {' '.join(args)} >> \"{log_file}\" 2>&1"
188
+ subprocess.Popen(["/bin/bash", "-c", cmd], start_new_session=True)
189
+
190
+ # The above commands kill us, so this line might not even log
191
+ logger.info("Restart signal sent. Goodbye!")
@@ -0,0 +1,117 @@
1
+ import asyncio
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from simstack.core.context import context
7
+ from simstack.core.services.base_service import BaseService
8
+ from simstack.models.parameters import Resource
9
+ from simstack.util.file_transfer_client import (
10
+ FileTransferClient,
11
+ FileTransferError,
12
+ resolve_instance_path,
13
+ )
14
+
15
+ logger = logging.getLogger("NodeRunner")
16
+
17
+
18
+ class FileTransferService(BaseService):
19
+ """Poll the SimStack server for hidden file transfer uploads assigned to this runner."""
20
+
21
+ def __init__(
22
+ self,
23
+ resource: Resource,
24
+ interval: int = 10,
25
+ max_concurrent: int = 2,
26
+ shutdown_event: asyncio.Event | None = None,
27
+ ) -> None:
28
+ super().__init__(
29
+ "FileTransfer", resource, interval, shutdown_event=shutdown_event
30
+ )
31
+ self._log_traceback_on_failure = False
32
+ self._semaphore = asyncio.Semaphore(max_concurrent)
33
+ self._running_tasks: set[asyncio.Task[bool]] = set()
34
+ self._client: FileTransferClient | None = None
35
+ self._configuration_checked = False
36
+
37
+ def _get_client(self) -> FileTransferClient | None:
38
+ if self._client is not None:
39
+ return self._client
40
+ if self._configuration_checked:
41
+ return None
42
+ self._configuration_checked = True
43
+ self._client = FileTransferClient.from_context(required=False)
44
+ if self._client is None:
45
+ logger.info(
46
+ "File transfer service disabled; SIMSTACK_SERVER_URL or SIMSTACK_RUNNER_TOKEN is not configured."
47
+ )
48
+ return self._client
49
+
50
+ async def execute(self) -> None:
51
+ completed_tasks = {task for task in self._running_tasks if task.done()}
52
+ for task in completed_tasks:
53
+ try:
54
+ await task
55
+ except Exception as exc:
56
+ logger.exception("File transfer task completed with error: %s", exc)
57
+ self._running_tasks.remove(task)
58
+
59
+ client = self._get_client()
60
+ if client is None:
61
+ return
62
+
63
+ try:
64
+ transfers = client.list_transfers(role="source", status="created", limit=10)
65
+ except Exception as exc:
66
+ raise exc
67
+
68
+ for transfer in transfers:
69
+ transfer_id = str(transfer.get("transfer_id") or "")
70
+ if not transfer_id:
71
+ continue
72
+ if any(
73
+ getattr(task, "transfer_id", None) == transfer_id
74
+ for task in self._running_tasks
75
+ ):
76
+ continue
77
+ task = asyncio.create_task(self._run_with_semaphore(client, transfer))
78
+ setattr(task, "transfer_id", transfer_id)
79
+ self._running_tasks.add(task)
80
+
81
+ async def _run_with_semaphore(
82
+ self, client: FileTransferClient, transfer: dict[str, Any]
83
+ ) -> bool:
84
+ async with self._semaphore:
85
+ return await asyncio.to_thread(self._upload_transfer, client, transfer)
86
+
87
+ def _upload_transfer(
88
+ self, client: FileTransferClient, transfer: dict[str, Any]
89
+ ) -> bool:
90
+ transfer_id = str(transfer.get("transfer_id"))
91
+ source_path = transfer.get("source_path")
92
+ if not source_path:
93
+ client.fail_transfer(
94
+ transfer_id,
95
+ error_message="Transfer is missing source_path metadata.",
96
+ error_code="SOURCE_PATH_MISSING",
97
+ )
98
+ return False
99
+
100
+ path = resolve_instance_path(str(source_path), Path(context.config.workdir))
101
+ try:
102
+ client.upload_file(transfer_id, path)
103
+ logger.info("Uploaded FileStack transfer %s from %s", transfer_id, path)
104
+ return True
105
+ except FileTransferError as exc:
106
+ logger.warning("File transfer upload failed for %s: %s", transfer_id, exc)
107
+ try:
108
+ client.fail_transfer(
109
+ transfer_id,
110
+ error_message=str(exc),
111
+ error_code="SOURCE_UPLOAD_FAILED",
112
+ )
113
+ except Exception:
114
+ logger.exception(
115
+ "Failed to report failed file transfer %s", transfer_id
116
+ )
117
+ return False
@@ -0,0 +1,12 @@
1
+ from simstack.models.parameters import Resource
2
+ from simstack.core.services.base_service import BaseService
3
+
4
+ class GitRestartService(BaseService):
5
+ """Service that runs an external script to check git/restart the runner"""
6
+
7
+ def __init__(self, resource: Resource, interval):
8
+ super().__init__("GitRestart", resource, interval)
9
+ self._resource_name = str(resource)
10
+
11
+ def execute(self):
12
+ raise NotImplementedError("GitRestartService must implement execute()")
@@ -0,0 +1,105 @@
1
+ import asyncio
2
+ import logging
3
+ from pathlib import Path
4
+ import hashlib
5
+
6
+ from simstack.core.context import context
7
+ from simstack.models.parameters import Resource
8
+ from simstack.models.runner_model import RunnerEventEnum
9
+ from simstack.core.services.base_service import RestartService
10
+
11
+ logger = logging.getLogger("NodeRunner")
12
+
13
+ def get_file_checksum(filepath: Path) -> str:
14
+ """Calculate SHA256 checksum of a file"""
15
+ if not filepath.exists():
16
+ return ""
17
+ hash_sha256 = hashlib.sha256()
18
+ with open(filepath, "rb") as f:
19
+ for chunk in iter(lambda: f.read(4096), b""):
20
+ hash_sha256.update(chunk)
21
+ return hash_sha256.hexdigest()
22
+
23
+ class GitUvUpdateService(RestartService):
24
+ """
25
+ Advanced service that performs git pull and uv lock upgrades.
26
+ If changes are detected, it restarts the runner.
27
+ """
28
+
29
+ def __init__(self, resource: Resource, interval):
30
+ super().__init__("GitUvUpdate", resource, interval)
31
+ # Resolve project root (assuming we are in src/simstack/core/runner.py)
32
+ self._project_dir = context.config.project_root.resolve(strict=True)
33
+ self._uv_lock_path = context.config.project_root / "uv.lock"
34
+ self._uv_extra_depencency_path = context.config.project_root / "user_extra_config.toml"
35
+ self.extras=False
36
+ self.desired_extras=[]
37
+ #check if the file _uv_extra_depencency_path exists if yes - read toml and then extras=True
38
+ try :
39
+ import tomllib
40
+ logger.info(f" trying to read user-specified (extras) optional dependencies from {self._uv_extra_depencency_path}")
41
+ with open (self._uv_extra_depencency_path,"rb") as f:
42
+ desired_extras_all=tomllib.load(f)
43
+ # Access the nested section - this was the issue with the keys
44
+ extras_section = desired_extras_all.get("optional_dependencies_desired_by_ressource", {})
45
+
46
+
47
+ # Access raw value without triggering validation against allowed_resources
48
+ resource_name = object.__getattribute__(resource, "__dict__").get("value") or str(resource)
49
+ #logger.info(f" name {resource_name} , type {type(resource_name)}, compared against {extras_section.keys()}")
50
+ if resource_name in extras_section.keys():
51
+ self.desired_extras = extras_section[resource_name]
52
+ self.extras = True
53
+
54
+ except Exception as e:
55
+ logger.warning(f"resource {resource} spec with user file {self._uv_extra_depencency_path} failed with the exception {e}")
56
+
57
+
58
+
59
+
60
+ async def _run_command(self, cmd: list, ignore_error=False) -> str:
61
+ """Run a shell command and return output"""
62
+ process = await asyncio.create_subprocess_exec(
63
+ *cmd,
64
+ cwd=str(self._project_dir),
65
+ stdout=asyncio.subprocess.PIPE,
66
+ stderr=asyncio.subprocess.PIPE
67
+ )
68
+ stdout, stderr = await process.communicate()
69
+ if process.returncode != 0:
70
+ if ignore_error:
71
+ logger.warning(f"Command {' '.join(cmd)} failed: {stderr.decode()}")
72
+ else:
73
+ raise RuntimeError(f"Command {' '.join(cmd)} failed: {stderr.decode()}")
74
+ return stdout.decode().strip()
75
+
76
+ async def execute(self):
77
+ # 1. Git Pull
78
+ # Check checksum before and after pull to see if Git brought a new lockfile
79
+ # old_uv_checksum = get_file_checksum(self._uv_lock_path)
80
+
81
+ # Ensure we don't have local lockfile changes that block the pull
82
+
83
+ await self._run_command(["git", "stash"], ignore_error=True)
84
+
85
+ git_output = await self._run_command(["git", "pull", "--recurse-submodules" ])
86
+ git_changed = "Already up to date." not in git_output
87
+
88
+ # Clear the stash now that we've pulled
89
+ # await self._run_command(["git", "stash", "drop"], ignore_error=True)
90
+
91
+ if git_changed: # or uv_locally_upgraded:
92
+ command_list=["uv", "sync", "--locked"]
93
+ if self.extras and len(self.desired_extras) > 0:
94
+
95
+ for desired_extra in self.desired_extras:
96
+ command_list.extend(["--extra", str(desired_extra)])
97
+ logger.info(f"user specified extra dependency {desired_extra}")
98
+
99
+ await self._run_command(command_list) # Update local .venv but with extras as specified by the user for the ressource
100
+ else:
101
+ await self._run_command(command_list) # Update local .venv
102
+ reason = "Git pull" if git_changed else "Local UV upgrade"
103
+ await self.write_resource_event(RunnerEventEnum.SHUTDOWN, message=reason)
104
+ logger.info(f"Update detected ({reason}). Triggering restart...")
105
+ await self.trigger_restart()
@@ -0,0 +1,182 @@
1
+ import asyncio
2
+ import logging
3
+ import platform
4
+ import subprocess
5
+
6
+ from simstack.core.context import context
7
+ from simstack.core.definitions import TaskStatus
8
+ from simstack.core.node import node_from_database
9
+ from simstack.core.node_claim import claim_submitted_node
10
+ from simstack.core.run_docker import run_docker
11
+ from simstack.core.services.base_service import BaseService
12
+ from simstack.core.submit_node import submit_node
13
+ from simstack.models import NodeRegistry
14
+ from simstack.models.parameters import Resource
15
+ from simstack.models.runner_model import RunnerEventEnum
16
+
17
+ logger = logging.getLogger("NodeRunner")
18
+
19
+
20
+ async def run_node_from_registry(registry_entry: NodeRegistry) -> bool:
21
+ # Create the node from the registry entry
22
+ node = await node_from_database(registry_entry)
23
+ if not node:
24
+ logger.error(
25
+ f"Failed to create node from registry entry task_id: {registry_entry.id} on resource {context.config.resource}"
26
+ )
27
+ registry_entry.status = TaskStatus.FAILED
28
+ await context.db.save(registry_entry)
29
+ return False
30
+ registry_entry = node.registry_entry # it may have changed
31
+ assert registry_entry is not None
32
+ if (
33
+ node.status == TaskStatus.RETRIEVED
34
+ or node.status == TaskStatus.SUBMITTED
35
+ or node.status == TaskStatus.SLURM_QUEUED
36
+ ) or (
37
+ node.status == TaskStatus.COMPLETED and registry_entry.parameters.force_rerun
38
+ ):
39
+ await node.execute_node_locally()
40
+ else:
41
+ logger.info(
42
+ f"task_id: {registry_entry.id} skipping task: {registry_entry.name} with status {registry_entry.status}"
43
+ )
44
+ return bool(node.status == TaskStatus.COMPLETED)
45
+
46
+
47
+ class NodeExecutionService(BaseService):
48
+ def __init__(
49
+ self,
50
+ resource: Resource,
51
+ interval: int,
52
+ max_concurrent: int,
53
+ shutdown_event: asyncio.Event | None,
54
+ detach: bool = True,
55
+ is_default: bool = False,
56
+ ) -> None:
57
+ super().__init__(
58
+ "JobPolling", resource, interval, shutdown_event=shutdown_event
59
+ )
60
+ self._resource_name = str(resource)
61
+ self._semaphore = asyncio.Semaphore(max_concurrent)
62
+ self._running_tasks: set[asyncio.Task[bool]] = set()
63
+ self._started = False
64
+ self._detach = detach
65
+ self._is_default = is_default
66
+
67
+ async def run_node(self, registry_entry: NodeRegistry) -> bool:
68
+ """Run a single node by its ID from the database"""
69
+
70
+ await self.write_node_event(RunnerEventEnum.NODE_STARTED, registry_entry.id)
71
+ try:
72
+ logger.info(
73
+ f"Running node task_id: {registry_entry.id} on resource {context.config.resource} with status {registry_entry.status}"
74
+ )
75
+ queue = (
76
+ registry_entry.parameters.queue
77
+ if hasattr(registry_entry.parameters, "queue")
78
+ else "default"
79
+ )
80
+ if queue is None:
81
+ logger.error(
82
+ f"Queue parameter not found for task_id: {registry_entry.id}"
83
+ )
84
+ return False
85
+
86
+ if queue == "slurm-queue":
87
+ return await submit_node(registry_entry)
88
+ elif queue == "docker":
89
+ return await run_docker(registry_entry)
90
+
91
+ elif queue == "default":
92
+ if self._detach:
93
+ # Spawn independent process that survives when the runner dies
94
+ cmd = [
95
+ "uv",
96
+ "run",
97
+ "--directory",
98
+ str(context.config.project_root),
99
+ "run_node",
100
+ "--node-id",
101
+ str(registry_entry.id),
102
+ "--resource",
103
+ str(self._resource_name),
104
+ ]
105
+
106
+ # Use platform specific flags to ensure the process survives if runner is killed
107
+ creationflags = 0
108
+ if platform.system() == "Windows":
109
+ create_new_process_group = getattr(
110
+ subprocess, "CREATE_NEW_PROCESS_GROUP", 0
111
+ )
112
+ detached_process = getattr(subprocess, "DETACHED_PROCESS", 0)
113
+ creationflags = create_new_process_group | detached_process
114
+
115
+ try:
116
+ process = await asyncio.create_subprocess_exec(
117
+ *cmd,
118
+ stdout=asyncio.subprocess.DEVNULL,
119
+ stderr=asyncio.subprocess.DEVNULL,
120
+ creationflags=creationflags,
121
+ start_new_session=True
122
+ if platform.system() != "Windows"
123
+ else False,
124
+ )
125
+ except Exception as e:
126
+ logger.error(
127
+ f"Failed to spawn detached process for task id: {registry_entry.id}. task_id: {registry_entry.id}. "
128
+ f"Command: {' '.join(cmd)}. Error: {str(e)}"
129
+ )
130
+ raise
131
+
132
+ logger.info(
133
+ f"Spawned detached process for task_id: {registry_entry.id} with PID: {process.pid}"
134
+ )
135
+ return True
136
+ else:
137
+ return await run_node_from_registry(registry_entry)
138
+ else:
139
+ logger.error(
140
+ f"Queue {queue} not supported for task_id: {registry_entry.id}"
141
+ )
142
+ return False
143
+
144
+ except Exception as e:
145
+ logger.exception(
146
+ f"Error running node task_id: {registry_entry.id} on resource {context.config.resource} : {str(e)}"
147
+ )
148
+ if registry_entry:
149
+ registry_entry.status = TaskStatus.FAILED
150
+ await context.db.save(registry_entry)
151
+ return False
152
+
153
+ async def execute(self) -> None:
154
+ if not self._started:
155
+ await self.write_resource_event(RunnerEventEnum.RUNNER_STARTED)
156
+ self._started = True
157
+
158
+ # Clean up the completed tasks
159
+ completed_tasks = {task for task in self._running_tasks if task.done()}
160
+ for task in completed_tasks:
161
+ try:
162
+ await task
163
+ except Exception as e:
164
+ logger.exception(f"Task completed with error: {e}")
165
+ self._running_tasks.remove(task)
166
+
167
+ # Load tasks
168
+ registry_entry_list = await context.db.load_waiting_tasks_for_resource(self._resource_name)
169
+
170
+ if registry_entry_list:
171
+ logger.info(
172
+ f"Retrieved {len(registry_entry_list)} tasks for {self._resource_name}"
173
+ )
174
+ for entry in registry_entry_list:
175
+ if not await claim_submitted_node(entry):
176
+ continue
177
+ task = asyncio.create_task(self._run_with_semaphore(entry))
178
+ self._running_tasks.add(task)
179
+
180
+ async def _run_with_semaphore(self, entry: NodeRegistry) -> bool:
181
+ async with self._semaphore:
182
+ return await self.run_node(entry)
@@ -0,0 +1,71 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ from simstack.core.context import context
5
+ from simstack.models.parameters import Resource
6
+ from simstack.models.runner_model import RunnerEventEnum
7
+ from simstack.models.resource_definition import ResourceDefinition
8
+ from simstack.core.services.base_service import RestartService
9
+
10
+ logger = logging.getLogger("NodeRunner")
11
+
12
+ class ResourceBranchMonitorService(RestartService):
13
+ """
14
+ Monitors the ResourceDefinition for the current resource.
15
+ If the git_branch field changes, it stashes, switches branch, syncs, and restarts.
16
+ """
17
+
18
+ def __init__(self, resource: Resource, interval: int):
19
+ super().__init__("ResourceBranchMonitor", resource, interval)
20
+ self._project_dir = context.config.project_root.resolve(strict=True)
21
+
22
+ async def _run_command(self, cmd: list) -> str:
23
+ process = await asyncio.create_subprocess_exec(
24
+ *cmd,
25
+ cwd=str(self._project_dir),
26
+ stdout=asyncio.subprocess.PIPE,
27
+ stderr=asyncio.subprocess.PIPE
28
+ )
29
+ stdout, stderr = await process.communicate()
30
+ if process.returncode != 0:
31
+ logger.error(f"Command {' '.join(cmd)} failed: {stderr.decode()}")
32
+ return ""
33
+ return stdout.decode().strip()
34
+
35
+ async def _get_current_branch(self) -> str:
36
+ return await self._run_command(["git", "rev-parse", "--abbrev-ref", "HEAD"])
37
+
38
+ async def execute(self):
39
+ resource_def = await context.db.find_one(
40
+ ResourceDefinition,
41
+ ResourceDefinition.resource_str == str(self._resource)
42
+ )
43
+
44
+ if not resource_def:
45
+ return
46
+
47
+ target_branch = resource_def.git_branch
48
+ current_branch = await self._get_current_branch()
49
+
50
+ if target_branch and current_branch and target_branch != current_branch:
51
+ logger.info(f"Branch change detected: {current_branch} -> {target_branch}. Updating...")
52
+
53
+ # 1. Stash existing changes
54
+ await self._run_command(["git", "stash"])
55
+
56
+ # 2. Checkout new branch
57
+ checkout_res = await self._run_command(["git", "checkout", target_branch])
58
+ if not checkout_res:
59
+ logger.error(f"Failed to checkout branch {target_branch}")
60
+ return
61
+
62
+ # 3. UV Sync
63
+ logger.info("Running uv sync --locked...")
64
+ await self._run_command(["uv", "sync", "--locked"])
65
+
66
+ # 4. Trigger Restart
67
+ await self.write_resource_event(
68
+ RunnerEventEnum.SHUTDOWN,
69
+ message=f"Branch switched to {target_branch}"
70
+ )
71
+ await self.trigger_restart()