railmux 0.4.0.dev27__tar.gz → 0.4.0.dev29__tar.gz

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 (181) hide show
  1. {railmux-0.4.0.dev27/src/railmux.egg-info → railmux-0.4.0.dev29}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev29/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_install_log.py +32 -3
  4. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_msys2.py +429 -23
  5. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29/src/railmux.egg-info}/PKG-INFO +1 -1
  6. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_install_log.py +52 -2
  7. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_msys2.py +371 -1
  8. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tools/windows_archive_smoke.py +54 -0
  9. railmux-0.4.0.dev27/src/railmux/__init__.py +0 -1
  10. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/LICENSE +0 -0
  11. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/MANIFEST.in +0 -0
  12. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/README.md +0 -0
  13. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/pyproject.toml +0 -0
  14. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/setup.cfg +0 -0
  15. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/__main__.py +0 -0
  16. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/atomic_file.py +0 -0
  17. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/background_index.py +0 -0
  18. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/cli.py +0 -0
  19. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/codex_index.py +0 -0
  20. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/config.py +0 -0
  21. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/config_cli.py +0 -0
  22. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/diagnostics.py +0 -0
  23. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/discovery.py +0 -0
  24. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/display_transport.py +0 -0
  25. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/entrypoint.py +0 -0
  26. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_client.py +0 -0
  27. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_history.py +0 -0
  28. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_input.py +0 -0
  29. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_protocol.py +0 -0
  30. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fast_display_server.py +0 -0
  31. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/favorites.py +0 -0
  32. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/fuzzy.py +0 -0
  33. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/help_workspace.py +0 -0
  34. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/launcher.py +0 -0
  35. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/legacy_sessions.py +0 -0
  36. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/local_clipboard.py +0 -0
  37. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/local_open.py +0 -0
  38. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/models.py +0 -0
  39. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/modes.py +0 -0
  40. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/mouse_manager.py +0 -0
  41. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/orphan_marker.py +0 -0
  42. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/pane_surface.py +0 -0
  43. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/path_codec.py +0 -0
  44. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/provider_paths.py +0 -0
  45. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/remote_config.py +0 -0
  46. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/renames.py +0 -0
  47. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/restart_state.py +0 -0
  48. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/runtime_config.py +0 -0
  49. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/scroll_agent.py +0 -0
  50. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/scroll_manager.py +0 -0
  51. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/selection_isolation.py +0 -0
  52. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/self_update.py +0 -0
  53. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/session_cache.py +0 -0
  54. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/session_index.py +0 -0
  55. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/session_lease.py +0 -0
  56. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/setting_contracts.py +0 -0
  57. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/settings.py +0 -0
  58. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ssh_args.py +0 -0
  59. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ssh_compat.py +0 -0
  60. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ssh_display_diagnostics.py +0 -0
  61. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ssh_doctor.py +0 -0
  62. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/system_deps.py +0 -0
  63. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/terminal_status.py +0 -0
  64. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tmux_binding_manager.py +0 -0
  65. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tmux_ctl.py +0 -0
  66. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tmux_health.py +0 -0
  67. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tmux_server.py +0 -0
  68. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/tool_panes.py +0 -0
  69. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/transcript.py +0 -0
  70. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/__init__.py +0 -0
  71. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/_widgets.py +0 -0
  72. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/app.py +0 -0
  73. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/keymap.py +0 -0
  74. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/modals.py +0 -0
  75. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/projects_pane.py +0 -0
  76. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/running_pane.py +0 -0
  77. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/sessions_pane.py +0 -0
  78. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/sidebar.py +0 -0
  79. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/statusbar.py +0 -0
  80. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/ui/workspace.py +0 -0
  81. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_attach_relay.py +0 -0
  82. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_bootstrap.py +0 -0
  83. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_pacman.py +0 -0
  84. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_tmux_lifecycle.py +0 -0
  85. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux/windows_ui_transition.py +0 -0
  86. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/SOURCES.txt +0 -0
  87. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/dependency_links.txt +0 -0
  88. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/entry_points.txt +0 -0
  89. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/requires.txt +0 -0
  90. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/src/railmux.egg-info/top_level.txt +0 -0
  91. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_agent_workspace.py +0 -0
  92. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_atomic_file.py +0 -0
  93. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_background_index.py +0 -0
  94. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_cli.py +0 -0
  95. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_codex_delete.py +0 -0
  96. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_codex_index.py +0 -0
  97. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_codex_lineage.py +0 -0
  98. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_codex_transcript.py +0 -0
  99. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_config.py +0 -0
  100. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_config_cli.py +0 -0
  101. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_denested_transport_benchmark.py +0 -0
  102. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_diagnostics.py +0 -0
  103. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_discovery.py +0 -0
  104. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_display_transport.py +0 -0
  105. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_fast_full_window.py +0 -0
  106. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_focus_highlight.py +0 -0
  107. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_fullscreen_binding.py +0 -0
  108. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_fuzzy.py +0 -0
  109. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_help_agent.py +0 -0
  110. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_help_workspace.py +0 -0
  111. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_history_cleanup.py +0 -0
  112. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_keymap.py +0 -0
  113. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_launcher.py +0 -0
  114. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_layout_profile.py +0 -0
  115. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_legacy_sessions.py +0 -0
  116. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_local_clipboard.py +0 -0
  117. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_local_open.py +0 -0
  118. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_modals.py +0 -0
  119. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_mode_selection.py +0 -0
  120. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_modes.py +0 -0
  121. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_mouse_manager.py +0 -0
  122. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_new_project.py +0 -0
  123. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_options.py +0 -0
  124. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_orphan_recovery.py +0 -0
  125. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_pane_surface.py +0 -0
  126. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_path_codec.py +0 -0
  127. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_projects_pane.py +0 -0
  128. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_provider_paths.py +0 -0
  129. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_release_notes.py +0 -0
  130. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_release_workflow.py +0 -0
  131. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_remote_config.py +0 -0
  132. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_renames.py +0 -0
  133. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_restart_state.py +0 -0
  134. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_running_pane.py +0 -0
  135. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_running_sort.py +0 -0
  136. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_runtime_config.py +0 -0
  137. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_scroll_agent.py +0 -0
  138. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_scroll_manager.py +0 -0
  139. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_selection_isolation.py +0 -0
  140. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_self_update.py +0 -0
  141. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_session_cache.py +0 -0
  142. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_session_index.py +0 -0
  143. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_session_lease.py +0 -0
  144. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_sessions_pane.py +0 -0
  145. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_setting_contracts.py +0 -0
  146. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_settings.py +0 -0
  147. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_sidebar.py +0 -0
  148. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_soft_quit.py +0 -0
  149. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_args.py +0 -0
  150. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_compat.py +0 -0
  151. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_display_benchmark.py +0 -0
  152. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_display_diagnostics.py +0 -0
  153. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_ssh_doctor.py +0 -0
  154. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_status.py +0 -0
  155. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_statusbar.py +0 -0
  156. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_system_deps.py +0 -0
  157. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_terminal_size.py +0 -0
  158. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_terminal_status.py +0 -0
  159. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_termux_local_touch.py +0 -0
  160. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_binding_manager.py +0 -0
  161. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_ctl.py +0 -0
  162. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_health.py +0 -0
  163. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_integration.py +0 -0
  164. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_server.py +0 -0
  165. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tmux_status.py +0 -0
  166. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_tool_panes.py +0 -0
  167. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_transcript.py +0 -0
  168. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_utf8_state.py +0 -0
  169. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_web_demo_recorder.py +0 -0
  170. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_wheel_smoke.py +0 -0
  171. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_widgets.py +0 -0
  172. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_attach_relay.py +0 -0
  173. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_bootstrap.py +0 -0
  174. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_pacman.py +0 -0
  175. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_tmux_lifecycle.py +0 -0
  176. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_ui_transition.py +0 -0
  177. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_windows_wrapper_contract.py +0 -0
  178. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tests/test_yolo_prompt.py +0 -0
  179. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tools/denested_transport_benchmark.py +0 -0
  180. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tools/release_notes.py +0 -0
  181. {railmux-0.4.0.dev27 → railmux-0.4.0.dev29}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev27
3
+ Version: 0.4.0.dev29
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0.dev29"
@@ -56,6 +56,11 @@ _PIP_NETWORK_ERROR_MARKERS = (
56
56
  "connection aborted",
57
57
  "temporary failure in name resolution",
58
58
  )
59
+ _MSYS2_RESTART_MARKER = (
60
+ "to complete this update all msys2 processes including this terminal "
61
+ "will be closed"
62
+ )
63
+ _ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
59
64
  _EXTRACTION_PERCENT_RE = re.compile(r"(?:^|\s)([0-9]{1,3})%\s")
60
65
  _PACKAGE_COUNT_RE = re.compile(r"^Packages \(([0-9]+)\)")
61
66
  _PACKAGE_DOWNLOAD_RE = re.compile(r"^\s*([^ ]+) downloading\.\.\.\s*$")
@@ -87,7 +92,12 @@ def install_log_path(
87
92
  )
88
93
 
89
94
 
90
- def _prune_install_logs(directory: Path, *, keep: int = _LOG_KEEP_COUNT) -> None:
95
+ def _prune_install_logs(
96
+ directory: Path,
97
+ *,
98
+ keep: int = _LOG_KEEP_COUNT,
99
+ preserve: Path | None = None,
100
+ ) -> None:
91
101
  candidates: list[Path] = []
92
102
  try:
93
103
  entries = list(directory.iterdir())
@@ -103,7 +113,11 @@ def _prune_install_logs(directory: Path, *, keep: int = _LOG_KEEP_COUNT) -> None
103
113
  )
104
114
  except OSError:
105
115
  return
106
- for stale in candidates[max(keep, 1) :]:
116
+ preserved = preserve if preserve in candidates else None
117
+ others = [candidate for candidate in candidates if candidate != preserved]
118
+ other_limit = max(keep, 1) - (1 if preserved is not None else 0)
119
+ stale_candidates = others[max(other_limit, 0) :]
120
+ for stale in stale_candidates:
107
121
  try:
108
122
  stale.unlink()
109
123
  except OSError:
@@ -142,12 +156,16 @@ class InstallReporter:
142
156
  self._network_failure = False
143
157
  self._pip_network_failure = False
144
158
  self._hard_failed_hosts: set[str] = set()
159
+ self._msys2_restart_requested = False
160
+ self._msys2_restart_lines: deque[str] = deque(maxlen=3)
145
161
 
146
162
  def __enter__(self) -> InstallReporter:
147
163
  self.path.parent.mkdir(parents=True, exist_ok=True)
148
164
  self._log = self.path.open("x", encoding="utf-8", newline="\n")
149
165
  self._write_log("Railmux Windows runtime installation\n")
150
- _prune_install_logs(self.path.parent)
166
+ # Windows may defer the visible last-write timestamp of an open file.
167
+ # Never let retention mistake the active evidence log for an old one.
168
+ _prune_install_logs(self.path.parent, preserve=self.path)
151
169
  return self
152
170
 
153
171
  def __exit__(self, _exc_type, _exc, _traceback) -> None:
@@ -209,6 +227,8 @@ class InstallReporter:
209
227
  self._network_failure = False
210
228
  self._pip_network_failure = False
211
229
  self._hard_failed_hosts.clear()
230
+ self._msys2_restart_requested = False
231
+ self._msys2_restart_lines.clear()
212
232
  self._write_log(f"\n--- {label} ---\n")
213
233
 
214
234
  def command_output(self, output: bytes | str | None) -> None:
@@ -259,6 +279,11 @@ class InstallReporter:
259
279
  if line.strip():
260
280
  self._tail.append(line)
261
281
  lowered = line.lower()
282
+ normalized = " ".join(_ANSI_ESCAPE_RE.sub("", lowered).split())
283
+ if normalized:
284
+ self._msys2_restart_lines.append(normalized)
285
+ if _MSYS2_RESTART_MARKER in " ".join(self._msys2_restart_lines):
286
+ self._msys2_restart_requested = True
262
287
  if any(marker in lowered for marker in _NETWORK_ERROR_MARKERS):
263
288
  self._network_failure = True
264
289
  if any(marker in lowered for marker in _PIP_NETWORK_ERROR_MARKERS):
@@ -364,6 +389,10 @@ class InstallReporter:
364
389
  def hard_failed_mirror_hosts(self) -> frozenset[str]:
365
390
  return frozenset(self._hard_failed_hosts)
366
391
 
392
+ @property
393
+ def command_requested_msys2_restart(self) -> bool:
394
+ return self._msys2_restart_requested
395
+
367
396
  def heartbeat(self) -> None:
368
397
  now = self._clock()
369
398
  if now - self._last_heartbeat < _HEARTBEAT_SECONDS:
@@ -119,6 +119,32 @@ _PACKAGE_URL_COMMAND = (
119
119
  _PACKAGE_INVENTORY_COMMAND = "set -o pipefail; pacman -Q | LC_ALL=C sort"
120
120
  _PACKAGE_INVENTORY_LIMIT = 1024 * 1024
121
121
  _CORE_RUNTIME_PACKAGES = ("tmux", "python", "python-pip")
122
+ _MAX_BASE_UPDATE_PASSES = 3
123
+ _PACMAN_LOG_RELATIVE = Path("var") / "log" / "pacman.log"
124
+ _PACMAN_LOG_MAX_SIZE = 8 * 1024 * 1024
125
+ _PACMAN_LOG_APPEND_LIMIT = 2 * 1024 * 1024
126
+ _PACMAN_LOCAL_DB_ENTRY_LIMIT = 4096
127
+ _PACMAN_LOCAL_DESC_LIMIT = 64 * 1024
128
+ # Keep this aligned with alpm_pkg_is_core_package in the pacman build shipped
129
+ # by the pinned base. Any one of these packages makes MSYS2 close its processes.
130
+ _PACMAN_CORE_PACKAGES = frozenset(
131
+ {
132
+ "bash",
133
+ "filesystem",
134
+ "mintty",
135
+ "msys2-runtime",
136
+ "msys2-runtime-devel",
137
+ "pacman",
138
+ "pacman-mirrors",
139
+ }
140
+ )
141
+ _PACMAN_CORE_CHANGE_RE = re.compile(
142
+ r"\[ALPM] (?:upgraded|downgraded) ([^ ]+) \(([^ ]+) -> ([^)]+)\)",
143
+ re.IGNORECASE,
144
+ )
145
+ _PRIVATE_GPG_SHUTDOWN_COMMAND = (
146
+ "gpgconf --homedir /etc/pacman.d/gnupg --kill all"
147
+ )
122
148
  _IN_USE_APPS_COMMAND = (
123
149
  "for f in /proc/[0-9]*/cmdline; do "
124
150
  "[ -r \"$f\" ] || continue; tr '\\0' '\\n' <\"$f\"; done"
@@ -196,6 +222,16 @@ ArchiveProbe = Callable[[str, str, int], _ArchiveProbe]
196
222
  ArchiveResume = Callable[[str, str, int, int, Callable[[bytes], None]], None]
197
223
 
198
224
 
225
+ @dataclass(frozen=True)
226
+ class _PacmanLogCheckpoint:
227
+ existed: bool
228
+ size: int
229
+ identity: tuple[int, int] | None
230
+ tail_offset: int
231
+ tail_digest: str
232
+ core_versions: tuple[tuple[str, str], ...]
233
+
234
+
199
235
  def _format_download_size(size: int) -> str:
200
236
  if size >= 1024 * 1024:
201
237
  return f"{size / (1024 * 1024):.1f} MiB"
@@ -1624,18 +1660,25 @@ def _run_checked(
1624
1660
  if returncode:
1625
1661
  if allow_failure:
1626
1662
  return returncode
1627
- if _NATIVE_WINDOWS and returncode & 0xFFFFFFFF == 0xC0000135:
1628
- reporter.note(
1629
- "Windows reported STATUS_DLL_NOT_FOUND. Check whether security "
1630
- "software quarantined a file in the private MSYS2 staging tree.",
1631
- level="error",
1632
- )
1663
+ _explain_windows_status(returncode, reporter)
1633
1664
  reporter.command_failed(label, returncode)
1634
1665
  raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
1635
1666
  reporter.command_succeeded()
1636
1667
  return 0
1637
1668
 
1638
1669
 
1670
+ def _explain_windows_status(returncode: int, reporter: InstallReporter) -> None:
1671
+ if _NATIVE_WINDOWS and returncode & 0xFFFFFFFF == 0xC0000135:
1672
+ reporter.note(
1673
+ "Windows reported STATUS_DLL_NOT_FOUND, but this pass had neither "
1674
+ "pacman's restart announcement nor a newly completed core "
1675
+ "transaction with a matching package-database version change. "
1676
+ "The temporary base will not be published; a missing or "
1677
+ "quarantined staged DLL remains possible.",
1678
+ level="error",
1679
+ )
1680
+
1681
+
1639
1682
  def _pacman_command(*, packages: bool, relaxed: bool) -> str:
1640
1683
  timeout = " --disable-download-timeout" if relaxed else ""
1641
1684
  needed = f" --needed {_PACMAN_PACKAGES}" if packages else ""
@@ -1644,10 +1687,11 @@ def _pacman_command(*, packages: bool, relaxed: bool) -> str:
1644
1687
  if not packages
1645
1688
  else ""
1646
1689
  )
1690
+ operation = "-Syu" if packages else "-Syuu"
1647
1691
  return (
1648
1692
  'cache=$(cygpath -u "$1") && mkdir -p "$cache" && '
1649
1693
  f"{keyring}pacman --config {_PACMAN_CONFIG} --cachedir \"$cache\" "
1650
- f"-Syu --noconfirm{timeout}{needed}"
1694
+ f"{operation} --noconfirm{timeout}{needed}"
1651
1695
  )
1652
1696
 
1653
1697
 
@@ -1666,6 +1710,215 @@ def _report_mirror_decision(
1666
1710
  )
1667
1711
 
1668
1712
 
1713
+ def _pacman_desc_value(text: str, field: str) -> str | None:
1714
+ lines = text.splitlines()
1715
+ marker = f"%{field}%"
1716
+ for index, line in enumerate(lines[:-1]):
1717
+ if line == marker and lines[index + 1]:
1718
+ return lines[index + 1]
1719
+ return None
1720
+
1721
+
1722
+ def _pacman_core_versions(root: Path) -> dict[str, str]:
1723
+ local_db = root / "var" / "lib" / "pacman" / "local"
1724
+ try:
1725
+ if _path_is_link_or_reparse(local_db) or not local_db.is_dir():
1726
+ raise RuntimeInstallError("the private pacman database is unsafe")
1727
+ entries = list(local_db.iterdir())
1728
+ except FileNotFoundError:
1729
+ return {}
1730
+ except OSError as exc:
1731
+ raise RuntimeInstallError("could not inspect the private pacman database") from exc
1732
+ if len(entries) > _PACMAN_LOCAL_DB_ENTRY_LIMIT:
1733
+ raise RuntimeInstallError("the private pacman database is unexpectedly large")
1734
+ versions: dict[str, str] = {}
1735
+ for entry in entries:
1736
+ if not any(
1737
+ entry.name.startswith(prefix)
1738
+ for prefix in (
1739
+ "bash-",
1740
+ "filesystem-",
1741
+ "mintty-",
1742
+ "msys2-runtime-",
1743
+ "pacman-",
1744
+ )
1745
+ ):
1746
+ continue
1747
+ desc = entry / "desc"
1748
+ try:
1749
+ if (
1750
+ _path_is_link_or_reparse(entry)
1751
+ or not entry.is_dir()
1752
+ or _path_is_link_or_reparse(desc)
1753
+ or not desc.is_file()
1754
+ or desc.stat().st_size > _PACMAN_LOCAL_DESC_LIMIT
1755
+ ):
1756
+ raise RuntimeInstallError("the private pacman database is unsafe")
1757
+ text = desc.read_text(encoding="utf-8")
1758
+ except RuntimeInstallError:
1759
+ raise
1760
+ except (OSError, UnicodeError) as exc:
1761
+ raise RuntimeInstallError(
1762
+ "could not read the private pacman database"
1763
+ ) from exc
1764
+ name = _pacman_desc_value(text, "NAME")
1765
+ version = _pacman_desc_value(text, "VERSION")
1766
+ if name is None or version is None or not _is_pacman_core_package(name):
1767
+ continue
1768
+ if name in versions:
1769
+ raise RuntimeInstallError(
1770
+ "the private pacman database contains duplicate core packages"
1771
+ )
1772
+ versions[name] = version
1773
+ return versions
1774
+
1775
+
1776
+ def _pacman_log_checkpoint(root: Path) -> _PacmanLogCheckpoint:
1777
+ """Pin the append-only pacman journal before a staged transaction."""
1778
+ path = root / _PACMAN_LOG_RELATIVE
1779
+ try:
1780
+ info = path.lstat()
1781
+ except FileNotFoundError:
1782
+ return _PacmanLogCheckpoint(
1783
+ False,
1784
+ 0,
1785
+ None,
1786
+ 0,
1787
+ hashlib.sha256().hexdigest(),
1788
+ tuple(sorted(_pacman_core_versions(root).items())),
1789
+ )
1790
+ except OSError as exc:
1791
+ raise RuntimeInstallError("could not inspect the private pacman log") from exc
1792
+ try:
1793
+ unsafe = _path_is_link_or_reparse(path)
1794
+ except OSError as exc:
1795
+ raise RuntimeInstallError("could not inspect the private pacman log") from exc
1796
+ if (
1797
+ unsafe
1798
+ or not stat.S_ISREG(info.st_mode)
1799
+ or info.st_size < 0
1800
+ or info.st_size > _PACMAN_LOG_MAX_SIZE
1801
+ ):
1802
+ raise RuntimeInstallError("the private pacman log is unsafe")
1803
+ tail_offset = max(0, info.st_size - 4096)
1804
+ try:
1805
+ with path.open("rb") as stream:
1806
+ stream.seek(tail_offset)
1807
+ tail = stream.read(info.st_size - tail_offset)
1808
+ except OSError as exc:
1809
+ raise RuntimeInstallError("could not read the private pacman log") from exc
1810
+ return _PacmanLogCheckpoint(
1811
+ True,
1812
+ info.st_size,
1813
+ (info.st_dev, info.st_ino),
1814
+ tail_offset,
1815
+ hashlib.sha256(tail).hexdigest(),
1816
+ tuple(sorted(_pacman_core_versions(root).items())),
1817
+ )
1818
+
1819
+
1820
+ def _is_pacman_core_package(name: str) -> bool:
1821
+ return name in _PACMAN_CORE_PACKAGES or name.startswith("msys2-runtime-")
1822
+
1823
+
1824
+ def _pacman_log_core_transaction(
1825
+ root: Path, checkpoint: _PacmanLogCheckpoint
1826
+ ) -> tuple[str, ...]:
1827
+ """Return core packages from one newly completed journal transaction."""
1828
+ path = root / _PACMAN_LOG_RELATIVE
1829
+ try:
1830
+ info = path.lstat()
1831
+ unsafe = _path_is_link_or_reparse(path)
1832
+ except OSError:
1833
+ return ()
1834
+ if (
1835
+ unsafe
1836
+ or not stat.S_ISREG(info.st_mode)
1837
+ or info.st_size < checkpoint.size
1838
+ or info.st_size > _PACMAN_LOG_MAX_SIZE
1839
+ or info.st_size - checkpoint.size > _PACMAN_LOG_APPEND_LIMIT
1840
+ ):
1841
+ return ()
1842
+ if checkpoint.existed and (info.st_dev, info.st_ino) != checkpoint.identity:
1843
+ return ()
1844
+ try:
1845
+ with path.open("rb") as stream:
1846
+ if checkpoint.existed:
1847
+ stream.seek(checkpoint.tail_offset)
1848
+ previous_tail = stream.read(checkpoint.size - checkpoint.tail_offset)
1849
+ if hashlib.sha256(previous_tail).hexdigest() != checkpoint.tail_digest:
1850
+ return ()
1851
+ stream.seek(checkpoint.size)
1852
+ appended = stream.read(_PACMAN_LOG_APPEND_LIMIT + 1)
1853
+ except OSError:
1854
+ return ()
1855
+ if not appended or len(appended) > _PACMAN_LOG_APPEND_LIMIT:
1856
+ return ()
1857
+
1858
+ core_started = False
1859
+ transaction_started = False
1860
+ changed: dict[str, tuple[str, str]] = {}
1861
+ for line in appended.decode("utf-8", errors="replace").splitlines():
1862
+ lowered = line.lower()
1863
+ if "[pacman] starting core system upgrade" in lowered:
1864
+ core_started = True
1865
+ transaction_started = False
1866
+ changed.clear()
1867
+ continue
1868
+ if not core_started:
1869
+ continue
1870
+ if "[alpm] transaction started" in lowered:
1871
+ transaction_started = True
1872
+ changed.clear()
1873
+ continue
1874
+ if not transaction_started:
1875
+ continue
1876
+ match = _PACMAN_CORE_CHANGE_RE.search(line)
1877
+ if match is not None and _is_pacman_core_package(match.group(1)):
1878
+ changed[match.group(1)] = (match.group(2), match.group(3))
1879
+ if "[alpm] transaction completed" in lowered:
1880
+ if changed:
1881
+ try:
1882
+ after = _pacman_core_versions(root)
1883
+ except RuntimeInstallError:
1884
+ return ()
1885
+ before = dict(checkpoint.core_versions)
1886
+ proven = tuple(
1887
+ sorted(
1888
+ name
1889
+ for name, (old_version, new_version) in changed.items()
1890
+ if before.get(name) == old_version
1891
+ and after.get(name) == new_version
1892
+ and old_version != new_version
1893
+ )
1894
+ )
1895
+ if proven:
1896
+ return proven
1897
+ transaction_started = False
1898
+ return ()
1899
+
1900
+
1901
+ def _restart_evidence(
1902
+ root: Path,
1903
+ checkpoint: _PacmanLogCheckpoint,
1904
+ reporter: InstallReporter,
1905
+ returncode: int,
1906
+ ) -> bool:
1907
+ if reporter.command_requested_msys2_restart:
1908
+ return True
1909
+ if returncode & 0xFFFFFFFF != 0xC0000135:
1910
+ return False
1911
+ packages = _pacman_log_core_transaction(root, checkpoint)
1912
+ if not packages:
1913
+ return False
1914
+ reporter.note(
1915
+ "The private pacman journal confirms a completed core update "
1916
+ f"({', '.join(packages)}); its final console prompt was not captured.",
1917
+ level="warning",
1918
+ )
1919
+ return True
1920
+
1921
+
1669
1922
  def _refresh_pacman_mirrors(
1670
1923
  root: Path,
1671
1924
  *,
@@ -1693,7 +1946,9 @@ def _run_pacman_with_recovery(
1693
1946
  runner: Runner | None,
1694
1947
  label: str,
1695
1948
  mirror_optimizer: MirrorOptimizer,
1696
- ) -> None:
1949
+ allow_core_restart: bool = False,
1950
+ ) -> bool:
1951
+ checkpoint = _pacman_log_checkpoint(root) if not packages else None
1697
1952
  argv = _bash_command(root, _pacman_command(packages=packages, relaxed=False), str(cache))
1698
1953
  returncode = _run_checked(
1699
1954
  argv,
@@ -1704,11 +1959,34 @@ def _run_pacman_with_recovery(
1704
1959
  progress="pacman",
1705
1960
  allow_failure=True,
1706
1961
  )
1962
+ restart_requested = (
1963
+ _restart_evidence(root, checkpoint, reporter, returncode)
1964
+ if checkpoint is not None
1965
+ else False
1966
+ )
1707
1967
  if returncode == 0:
1708
- return
1968
+ return restart_requested
1969
+ if not packages and restart_requested and not allow_core_restart:
1970
+ reporter.note(
1971
+ "MSYS2 requested another core restart on the final permitted "
1972
+ "update pass; the temporary base will not be published.",
1973
+ level="error",
1974
+ )
1975
+ reporter.command_failed(label, returncode)
1976
+ raise RuntimeInstallError(
1977
+ f"{label} exhausted the permitted core-update restarts"
1978
+ )
1979
+ if allow_core_restart and not packages and restart_requested:
1980
+ reporter.note(
1981
+ "MSYS2 replaced core runtime files and closed its own process as "
1982
+ "expected; continuing from a fresh Windows-launched shell.",
1983
+ level="warning",
1984
+ )
1985
+ return True
1709
1986
  network_failure = reporter.command_had_network_failure
1710
1987
  hard_failed_hosts = reporter.hard_failed_mirror_hosts
1711
1988
  if not network_failure:
1989
+ _explain_windows_status(returncode, reporter)
1712
1990
  reporter.command_failed(label, returncode)
1713
1991
  raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
1714
1992
  reporter.note(
@@ -1739,13 +2017,128 @@ def _run_pacman_with_recovery(
1739
2017
  _pacman_command(packages=packages, relaxed=True),
1740
2018
  str(cache),
1741
2019
  )
1742
- _run_checked(
2020
+ retry_checkpoint = _pacman_log_checkpoint(root) if not packages else None
2021
+ retry_returncode = _run_checked(
1743
2022
  retry_argv,
1744
2023
  env=env,
1745
2024
  reporter=reporter,
1746
2025
  runner=runner,
1747
2026
  label=f"{label} resilient retry",
1748
2027
  progress="pacman",
2028
+ allow_failure=True,
2029
+ )
2030
+ restart_requested = (
2031
+ _restart_evidence(root, retry_checkpoint, reporter, retry_returncode)
2032
+ if retry_checkpoint is not None
2033
+ else False
2034
+ )
2035
+ if retry_returncode == 0:
2036
+ return restart_requested
2037
+ if not packages and restart_requested and not allow_core_restart:
2038
+ reporter.note(
2039
+ "MSYS2 requested another core restart on the final permitted "
2040
+ "update pass; the temporary base will not be published.",
2041
+ level="error",
2042
+ )
2043
+ reporter.command_failed(f"{label} resilient retry", retry_returncode)
2044
+ raise RuntimeInstallError(
2045
+ f"{label} resilient retry exhausted the permitted "
2046
+ "core-update restarts"
2047
+ )
2048
+ if allow_core_restart and not packages and restart_requested:
2049
+ reporter.note(
2050
+ "MSYS2 replaced core runtime files and closed its own process as "
2051
+ "expected; continuing from a fresh Windows-launched shell.",
2052
+ level="warning",
2053
+ )
2054
+ return True
2055
+ _explain_windows_status(retry_returncode, reporter)
2056
+ reporter.command_failed(f"{label} resilient retry", retry_returncode)
2057
+ raise RuntimeInstallError(
2058
+ f"{label} resilient retry failed with exit code {retry_returncode}"
2059
+ )
2060
+
2061
+
2062
+ def _run_base_update_with_restarts(
2063
+ root: Path,
2064
+ *,
2065
+ cache: Path,
2066
+ env: Mapping[str, str],
2067
+ reporter: InstallReporter,
2068
+ runner: Runner | None,
2069
+ mirror_optimizer: MirrorOptimizer,
2070
+ ) -> None:
2071
+ """Run MSYS2's full upgrade in fresh processes until restart-safe."""
2072
+ try:
2073
+ for pass_number in range(1, _MAX_BASE_UPDATE_PASSES + 1):
2074
+ restart_requested = _run_pacman_with_recovery(
2075
+ root,
2076
+ packages=False,
2077
+ cache=cache,
2078
+ env=env,
2079
+ reporter=reporter,
2080
+ runner=runner,
2081
+ label=f"MSYS2 base update pass {pass_number}",
2082
+ mirror_optimizer=mirror_optimizer,
2083
+ allow_core_restart=pass_number < _MAX_BASE_UPDATE_PASSES,
2084
+ )
2085
+ if restart_requested:
2086
+ reporter.note(
2087
+ "Starting a new MSYS2 process for the remaining update…"
2088
+ )
2089
+ continue
2090
+ if pass_number == 1:
2091
+ # MSYS2's supported unattended update procedure invokes Syuu
2092
+ # twice, even when the first pass did not request a restart.
2093
+ reporter.note(
2094
+ "Starting the required second MSYS2 update pass in a new "
2095
+ "process…"
2096
+ )
2097
+ continue
2098
+ return
2099
+ raise RuntimeInstallError(
2100
+ "MSYS2 repeatedly requested a core-runtime restart; the temporary "
2101
+ "base was not published"
2102
+ )
2103
+ finally:
2104
+ _stop_private_gpg_agents(
2105
+ root,
2106
+ env=env,
2107
+ reporter=reporter,
2108
+ runner=runner,
2109
+ strict=sys.exc_info()[0] is None,
2110
+ )
2111
+
2112
+
2113
+ def _stop_private_gpg_agents(
2114
+ root: Path,
2115
+ *,
2116
+ env: Mapping[str, str],
2117
+ reporter: InstallReporter,
2118
+ runner: Runner | None,
2119
+ strict: bool,
2120
+ ) -> None:
2121
+ returncode = _run_checked(
2122
+ _bash_command(root, _PRIVATE_GPG_SHUTDOWN_COMMAND),
2123
+ env=env,
2124
+ reporter=reporter,
2125
+ runner=runner,
2126
+ label="Stopping private MSYS2 keyring agents",
2127
+ allow_failure=True,
2128
+ )
2129
+ if returncode == 0:
2130
+ return
2131
+ if not strict:
2132
+ reporter.note(
2133
+ "Could not stop a private MSYS2 keyring agent while recovering "
2134
+ "from another installation failure.",
2135
+ level="warning",
2136
+ )
2137
+ return
2138
+ reporter.command_failed("Stopping private MSYS2 keyring agents", returncode)
2139
+ raise RuntimeInstallError(
2140
+ "stopping private MSYS2 keyring agents failed with exit code "
2141
+ f"{returncode}"
1749
2142
  )
1750
2143
 
1751
2144
 
@@ -2399,16 +2792,20 @@ def install_managed_runtime(
2399
2792
  "pacman is noninteractive; displayed [Y/n] prompts do "
2400
2793
  "not require input."
2401
2794
  )
2795
+ reporter.note(
2796
+ "Core updates may close the first MSYS2 process; "
2797
+ "Railmux will start the required fresh process "
2798
+ "automatically.",
2799
+ level="muted",
2800
+ )
2402
2801
  package_cache = cache_base / f"pacman-{MSYS2_RUNTIME_ID}"
2403
2802
  package_cache.mkdir(parents=True, exist_ok=True)
2404
- _run_pacman_with_recovery(
2803
+ _run_base_update_with_restarts(
2405
2804
  root,
2406
- packages=False,
2407
2805
  cache=package_cache,
2408
2806
  env=child_env,
2409
2807
  reporter=reporter,
2410
2808
  runner=runner,
2411
- label="MSYS2 base update",
2412
2809
  mirror_optimizer=mirror_optimizer,
2413
2810
  )
2414
2811
  reporter.done()
@@ -2445,16 +2842,25 @@ def install_managed_runtime(
2445
2842
  reporter=reporter,
2446
2843
  runner=runner,
2447
2844
  )
2448
- _run_pacman_with_recovery(
2449
- root,
2450
- packages=True,
2451
- cache=package_cache,
2452
- env=child_env,
2453
- reporter=reporter,
2454
- runner=runner,
2455
- label="MSYS2 package installation",
2456
- mirror_optimizer=mirror_optimizer,
2457
- )
2845
+ try:
2846
+ _run_pacman_with_recovery(
2847
+ root,
2848
+ packages=True,
2849
+ cache=package_cache,
2850
+ env=child_env,
2851
+ reporter=reporter,
2852
+ runner=runner,
2853
+ label="MSYS2 package installation",
2854
+ mirror_optimizer=mirror_optimizer,
2855
+ )
2856
+ finally:
2857
+ _stop_private_gpg_agents(
2858
+ root,
2859
+ env=child_env,
2860
+ reporter=reporter,
2861
+ runner=runner,
2862
+ strict=sys.exc_info()[0] is None,
2863
+ )
2458
2864
  reporter.done()
2459
2865
 
2460
2866
  identity = _ensure_base_content_identity(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev27
3
+ Version: 0.4.0.dev29
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT