railmux 0.4.0.dev26__tar.gz → 0.4.0.dev28__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.dev26 → railmux-0.4.0.dev28}/MANIFEST.in +1 -0
  2. {railmux-0.4.0.dev26/src/railmux.egg-info → railmux-0.4.0.dev28}/PKG-INFO +1 -1
  3. railmux-0.4.0.dev28/src/railmux/__init__.py +1 -0
  4. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_install_log.py +12 -0
  5. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_msys2.py +168 -19
  6. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28/src/railmux.egg-info}/PKG-INFO +1 -1
  7. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/SOURCES.txt +2 -1
  8. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_install_log.py +41 -0
  9. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_msys2.py +142 -5
  10. railmux-0.4.0.dev28/tools/windows_archive_smoke.py +85 -0
  11. railmux-0.4.0.dev26/src/railmux/__init__.py +0 -1
  12. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/LICENSE +0 -0
  13. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/README.md +0 -0
  14. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/pyproject.toml +0 -0
  15. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/setup.cfg +0 -0
  16. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/__main__.py +0 -0
  17. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/atomic_file.py +0 -0
  18. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/background_index.py +0 -0
  19. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/cli.py +0 -0
  20. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/codex_index.py +0 -0
  21. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/config.py +0 -0
  22. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/config_cli.py +0 -0
  23. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/diagnostics.py +0 -0
  24. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/discovery.py +0 -0
  25. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/display_transport.py +0 -0
  26. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/entrypoint.py +0 -0
  27. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_client.py +0 -0
  28. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_history.py +0 -0
  29. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_input.py +0 -0
  30. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_protocol.py +0 -0
  31. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fast_display_server.py +0 -0
  32. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/favorites.py +0 -0
  33. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/fuzzy.py +0 -0
  34. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/help_workspace.py +0 -0
  35. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/launcher.py +0 -0
  36. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/legacy_sessions.py +0 -0
  37. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/local_clipboard.py +0 -0
  38. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/local_open.py +0 -0
  39. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/models.py +0 -0
  40. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/modes.py +0 -0
  41. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/mouse_manager.py +0 -0
  42. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/orphan_marker.py +0 -0
  43. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/pane_surface.py +0 -0
  44. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/path_codec.py +0 -0
  45. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/provider_paths.py +0 -0
  46. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/remote_config.py +0 -0
  47. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/renames.py +0 -0
  48. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/restart_state.py +0 -0
  49. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/runtime_config.py +0 -0
  50. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/scroll_agent.py +0 -0
  51. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/scroll_manager.py +0 -0
  52. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/selection_isolation.py +0 -0
  53. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/self_update.py +0 -0
  54. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/session_cache.py +0 -0
  55. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/session_index.py +0 -0
  56. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/session_lease.py +0 -0
  57. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/setting_contracts.py +0 -0
  58. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/settings.py +0 -0
  59. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ssh_args.py +0 -0
  60. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ssh_compat.py +0 -0
  61. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ssh_display_diagnostics.py +0 -0
  62. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ssh_doctor.py +0 -0
  63. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/system_deps.py +0 -0
  64. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/terminal_status.py +0 -0
  65. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tmux_binding_manager.py +0 -0
  66. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tmux_ctl.py +0 -0
  67. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tmux_health.py +0 -0
  68. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tmux_server.py +0 -0
  69. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/tool_panes.py +0 -0
  70. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/transcript.py +0 -0
  71. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/__init__.py +0 -0
  72. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/_widgets.py +0 -0
  73. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/app.py +0 -0
  74. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/keymap.py +0 -0
  75. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/modals.py +0 -0
  76. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/projects_pane.py +0 -0
  77. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/running_pane.py +0 -0
  78. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/sessions_pane.py +0 -0
  79. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/sidebar.py +0 -0
  80. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/statusbar.py +0 -0
  81. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/ui/workspace.py +0 -0
  82. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_attach_relay.py +0 -0
  83. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_bootstrap.py +0 -0
  84. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_pacman.py +0 -0
  85. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_tmux_lifecycle.py +0 -0
  86. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux/windows_ui_transition.py +0 -0
  87. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/dependency_links.txt +0 -0
  88. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/entry_points.txt +0 -0
  89. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/requires.txt +0 -0
  90. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/src/railmux.egg-info/top_level.txt +0 -0
  91. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_agent_workspace.py +0 -0
  92. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_atomic_file.py +0 -0
  93. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_background_index.py +0 -0
  94. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_cli.py +0 -0
  95. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_codex_delete.py +0 -0
  96. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_codex_index.py +0 -0
  97. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_codex_lineage.py +0 -0
  98. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_codex_transcript.py +0 -0
  99. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_config.py +0 -0
  100. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_config_cli.py +0 -0
  101. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_denested_transport_benchmark.py +0 -0
  102. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_diagnostics.py +0 -0
  103. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_discovery.py +0 -0
  104. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_display_transport.py +0 -0
  105. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_fast_full_window.py +0 -0
  106. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_focus_highlight.py +0 -0
  107. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_fullscreen_binding.py +0 -0
  108. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_fuzzy.py +0 -0
  109. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_help_agent.py +0 -0
  110. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_help_workspace.py +0 -0
  111. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_history_cleanup.py +0 -0
  112. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_keymap.py +0 -0
  113. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_launcher.py +0 -0
  114. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_layout_profile.py +0 -0
  115. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_legacy_sessions.py +0 -0
  116. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_local_clipboard.py +0 -0
  117. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_local_open.py +0 -0
  118. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_modals.py +0 -0
  119. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_mode_selection.py +0 -0
  120. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_modes.py +0 -0
  121. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_mouse_manager.py +0 -0
  122. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_new_project.py +0 -0
  123. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_options.py +0 -0
  124. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_orphan_recovery.py +0 -0
  125. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_pane_surface.py +0 -0
  126. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_path_codec.py +0 -0
  127. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_projects_pane.py +0 -0
  128. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_provider_paths.py +0 -0
  129. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_release_notes.py +0 -0
  130. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_release_workflow.py +0 -0
  131. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_remote_config.py +0 -0
  132. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_renames.py +0 -0
  133. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_restart_state.py +0 -0
  134. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_running_pane.py +0 -0
  135. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_running_sort.py +0 -0
  136. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_runtime_config.py +0 -0
  137. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_scroll_agent.py +0 -0
  138. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_scroll_manager.py +0 -0
  139. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_selection_isolation.py +0 -0
  140. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_self_update.py +0 -0
  141. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_session_cache.py +0 -0
  142. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_session_index.py +0 -0
  143. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_session_lease.py +0 -0
  144. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_sessions_pane.py +0 -0
  145. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_setting_contracts.py +0 -0
  146. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_settings.py +0 -0
  147. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_sidebar.py +0 -0
  148. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_soft_quit.py +0 -0
  149. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_args.py +0 -0
  150. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_compat.py +0 -0
  151. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_display_benchmark.py +0 -0
  152. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_display_diagnostics.py +0 -0
  153. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_ssh_doctor.py +0 -0
  154. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_status.py +0 -0
  155. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_statusbar.py +0 -0
  156. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_system_deps.py +0 -0
  157. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_terminal_size.py +0 -0
  158. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_terminal_status.py +0 -0
  159. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_termux_local_touch.py +0 -0
  160. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_binding_manager.py +0 -0
  161. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_ctl.py +0 -0
  162. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_health.py +0 -0
  163. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_integration.py +0 -0
  164. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_server.py +0 -0
  165. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tmux_status.py +0 -0
  166. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_tool_panes.py +0 -0
  167. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_transcript.py +0 -0
  168. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_utf8_state.py +0 -0
  169. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_web_demo_recorder.py +0 -0
  170. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_wheel_smoke.py +0 -0
  171. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_widgets.py +0 -0
  172. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_attach_relay.py +0 -0
  173. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_bootstrap.py +0 -0
  174. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_pacman.py +0 -0
  175. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_tmux_lifecycle.py +0 -0
  176. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_ui_transition.py +0 -0
  177. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_windows_wrapper_contract.py +0 -0
  178. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tests/test_yolo_prompt.py +0 -0
  179. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tools/denested_transport_benchmark.py +0 -0
  180. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tools/release_notes.py +0 -0
  181. {railmux-0.4.0.dev26 → railmux-0.4.0.dev28}/tools/wheel_smoke.py +0 -0
@@ -1,3 +1,4 @@
1
1
  include tools/denested_transport_benchmark.py
2
2
  include tools/release_notes.py
3
3
  include tools/wheel_smoke.py
4
+ include tools/windows_archive_smoke.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev26
3
+ Version: 0.4.0.dev28
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.dev28"
@@ -56,6 +56,10 @@ _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
+ )
59
63
  _EXTRACTION_PERCENT_RE = re.compile(r"(?:^|\s)([0-9]{1,3})%\s")
60
64
  _PACKAGE_COUNT_RE = re.compile(r"^Packages \(([0-9]+)\)")
61
65
  _PACKAGE_DOWNLOAD_RE = re.compile(r"^\s*([^ ]+) downloading\.\.\.\s*$")
@@ -142,6 +146,7 @@ class InstallReporter:
142
146
  self._network_failure = False
143
147
  self._pip_network_failure = False
144
148
  self._hard_failed_hosts: set[str] = set()
149
+ self._msys2_restart_requested = False
145
150
 
146
151
  def __enter__(self) -> InstallReporter:
147
152
  self.path.parent.mkdir(parents=True, exist_ok=True)
@@ -209,6 +214,7 @@ class InstallReporter:
209
214
  self._network_failure = False
210
215
  self._pip_network_failure = False
211
216
  self._hard_failed_hosts.clear()
217
+ self._msys2_restart_requested = False
212
218
  self._write_log(f"\n--- {label} ---\n")
213
219
 
214
220
  def command_output(self, output: bytes | str | None) -> None:
@@ -259,6 +265,8 @@ class InstallReporter:
259
265
  if line.strip():
260
266
  self._tail.append(line)
261
267
  lowered = line.lower()
268
+ if _MSYS2_RESTART_MARKER in lowered:
269
+ self._msys2_restart_requested = True
262
270
  if any(marker in lowered for marker in _NETWORK_ERROR_MARKERS):
263
271
  self._network_failure = True
264
272
  if any(marker in lowered for marker in _PIP_NETWORK_ERROR_MARKERS):
@@ -364,6 +372,10 @@ class InstallReporter:
364
372
  def hard_failed_mirror_hosts(self) -> frozenset[str]:
365
373
  return frozenset(self._hard_failed_hosts)
366
374
 
375
+ @property
376
+ def command_requested_msys2_restart(self) -> bool:
377
+ return self._msys2_restart_requested
378
+
367
379
  def heartbeat(self) -> None:
368
380
  now = self._clock()
369
381
  if now - self._last_heartbeat < _HEARTBEAT_SECONDS:
@@ -80,6 +80,7 @@ MSYS2_BASE_LINEAGE_SHA256 = (
80
80
  "6fe0cc8154132040e034ff4daface2a4163a9d1f6ebaaa1133394bff460bd5cf"
81
81
  )
82
82
  MSYS2_RUNTIME_ID = f"msys2-{MSYS2_RELEASE}"
83
+ _NATIVE_WINDOWS = os.name == "nt"
83
84
  RUNTIME_SCHEMA = 1
84
85
 
85
86
  _RUNTIME_OVERRIDE = "RAILMUX_MSYS2_ROOT"
@@ -118,6 +119,10 @@ _PACKAGE_URL_COMMAND = (
118
119
  _PACKAGE_INVENTORY_COMMAND = "set -o pipefail; pacman -Q | LC_ALL=C sort"
119
120
  _PACKAGE_INVENTORY_LIMIT = 1024 * 1024
120
121
  _CORE_RUNTIME_PACKAGES = ("tmux", "python", "python-pip")
122
+ _MAX_BASE_UPDATE_PASSES = 3
123
+ _PRIVATE_GPG_SHUTDOWN_COMMAND = (
124
+ "gpgconf --homedir /etc/pacman.d/gnupg --kill all"
125
+ )
121
126
  _IN_USE_APPS_COMMAND = (
122
127
  "for f in /proc/[0-9]*/cmdline; do "
123
128
  "[ -r \"$f\" ] || continue; tr '\\0' '\\n' <\"$f\"; done"
@@ -621,6 +626,16 @@ def _ensure_safe_archive_directory(root: Path, parts: Sequence[str]) -> Path:
621
626
  return current
622
627
 
623
628
 
629
+ def _apply_archive_mode(path: Path, mode: int) -> None:
630
+ if _NATIVE_WINDOWS:
631
+ # Windows chmod maps a missing owner-write bit to the NTFS read-only
632
+ # attribute; applying tar's POSIX 0444/0555 modes would prevent pacman
633
+ # from replacing those files during the first base update.
634
+ os.chmod(path, stat.S_IWRITE)
635
+ else:
636
+ os.chmod(path, mode)
637
+
638
+
624
639
  def _extract_msys2_archive(
625
640
  archive: Path,
626
641
  destination: Path,
@@ -696,7 +711,7 @@ def _extract_msys2_archive(
696
711
  )
697
712
  output.write(chunk)
698
713
  remaining -= len(chunk)
699
- os.chmod(target, member.mode & 0o777)
714
+ _apply_archive_mode(target, member.mode & 0o777)
700
715
  except RuntimeInstallError:
701
716
  raise
702
717
  except OSError as exc:
@@ -715,7 +730,7 @@ def _extract_msys2_archive(
715
730
  "the verified MSYS2 archive expanded to an unexpected size"
716
731
  )
717
732
  for directory, mode in reversed(directories):
718
- os.chmod(directory, mode)
733
+ _apply_archive_mode(directory, mode)
719
734
  except RuntimeInstallError:
720
735
  reporter.note(
721
736
  "MSYS2 archive extraction failed; the temporary base was not "
@@ -1613,12 +1628,23 @@ def _run_checked(
1613
1628
  if returncode:
1614
1629
  if allow_failure:
1615
1630
  return returncode
1631
+ _explain_windows_status(returncode, reporter)
1616
1632
  reporter.command_failed(label, returncode)
1617
1633
  raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
1618
1634
  reporter.command_succeeded()
1619
1635
  return 0
1620
1636
 
1621
1637
 
1638
+ def _explain_windows_status(returncode: int, reporter: InstallReporter) -> None:
1639
+ if _NATIVE_WINDOWS and returncode & 0xFFFFFFFF == 0xC0000135:
1640
+ reporter.note(
1641
+ "Windows reported STATUS_DLL_NOT_FOUND outside a confirmed MSYS2 "
1642
+ "core-update restart. Check whether security software quarantined "
1643
+ "a file in the private MSYS2 staging tree.",
1644
+ level="error",
1645
+ )
1646
+
1647
+
1622
1648
  def _pacman_command(*, packages: bool, relaxed: bool) -> str:
1623
1649
  timeout = " --disable-download-timeout" if relaxed else ""
1624
1650
  needed = f" --needed {_PACMAN_PACKAGES}" if packages else ""
@@ -1627,10 +1653,11 @@ def _pacman_command(*, packages: bool, relaxed: bool) -> str:
1627
1653
  if not packages
1628
1654
  else ""
1629
1655
  )
1656
+ operation = "-Syu" if packages else "-Syuu"
1630
1657
  return (
1631
1658
  'cache=$(cygpath -u "$1") && mkdir -p "$cache" && '
1632
1659
  f"{keyring}pacman --config {_PACMAN_CONFIG} --cachedir \"$cache\" "
1633
- f"-Syu --noconfirm{timeout}{needed}"
1660
+ f"{operation} --noconfirm{timeout}{needed}"
1634
1661
  )
1635
1662
 
1636
1663
 
@@ -1676,7 +1703,8 @@ def _run_pacman_with_recovery(
1676
1703
  runner: Runner | None,
1677
1704
  label: str,
1678
1705
  mirror_optimizer: MirrorOptimizer,
1679
- ) -> None:
1706
+ allow_core_restart: bool = False,
1707
+ ) -> bool:
1680
1708
  argv = _bash_command(root, _pacman_command(packages=packages, relaxed=False), str(cache))
1681
1709
  returncode = _run_checked(
1682
1710
  argv,
@@ -1687,11 +1715,20 @@ def _run_pacman_with_recovery(
1687
1715
  progress="pacman",
1688
1716
  allow_failure=True,
1689
1717
  )
1718
+ restart_requested = reporter.command_requested_msys2_restart
1690
1719
  if returncode == 0:
1691
- return
1720
+ return restart_requested
1721
+ if allow_core_restart and not packages and restart_requested:
1722
+ reporter.note(
1723
+ "MSYS2 replaced core runtime files and closed its own process as "
1724
+ "expected; continuing from a fresh Windows-launched shell.",
1725
+ level="warning",
1726
+ )
1727
+ return True
1692
1728
  network_failure = reporter.command_had_network_failure
1693
1729
  hard_failed_hosts = reporter.hard_failed_mirror_hosts
1694
1730
  if not network_failure:
1731
+ _explain_windows_status(returncode, reporter)
1695
1732
  reporter.command_failed(label, returncode)
1696
1733
  raise RuntimeInstallError(f"{label} failed with exit code {returncode}")
1697
1734
  reporter.note(
@@ -1722,13 +1759,112 @@ def _run_pacman_with_recovery(
1722
1759
  _pacman_command(packages=packages, relaxed=True),
1723
1760
  str(cache),
1724
1761
  )
1725
- _run_checked(
1762
+ retry_returncode = _run_checked(
1726
1763
  retry_argv,
1727
1764
  env=env,
1728
1765
  reporter=reporter,
1729
1766
  runner=runner,
1730
1767
  label=f"{label} resilient retry",
1731
1768
  progress="pacman",
1769
+ allow_failure=True,
1770
+ )
1771
+ restart_requested = reporter.command_requested_msys2_restart
1772
+ if retry_returncode == 0:
1773
+ return restart_requested
1774
+ if allow_core_restart and not packages and restart_requested:
1775
+ reporter.note(
1776
+ "MSYS2 replaced core runtime files and closed its own process as "
1777
+ "expected; continuing from a fresh Windows-launched shell.",
1778
+ level="warning",
1779
+ )
1780
+ return True
1781
+ _explain_windows_status(retry_returncode, reporter)
1782
+ reporter.command_failed(f"{label} resilient retry", retry_returncode)
1783
+ raise RuntimeInstallError(
1784
+ f"{label} resilient retry failed with exit code {retry_returncode}"
1785
+ )
1786
+
1787
+
1788
+ def _run_base_update_with_restarts(
1789
+ root: Path,
1790
+ *,
1791
+ cache: Path,
1792
+ env: Mapping[str, str],
1793
+ reporter: InstallReporter,
1794
+ runner: Runner | None,
1795
+ mirror_optimizer: MirrorOptimizer,
1796
+ ) -> None:
1797
+ """Run MSYS2's full upgrade in fresh processes until restart-safe."""
1798
+ try:
1799
+ for pass_number in range(1, _MAX_BASE_UPDATE_PASSES + 1):
1800
+ restart_requested = _run_pacman_with_recovery(
1801
+ root,
1802
+ packages=False,
1803
+ cache=cache,
1804
+ env=env,
1805
+ reporter=reporter,
1806
+ runner=runner,
1807
+ label=f"MSYS2 base update pass {pass_number}",
1808
+ mirror_optimizer=mirror_optimizer,
1809
+ allow_core_restart=pass_number < _MAX_BASE_UPDATE_PASSES,
1810
+ )
1811
+ if restart_requested:
1812
+ reporter.note(
1813
+ "Starting a new MSYS2 process for the remaining update…"
1814
+ )
1815
+ continue
1816
+ if pass_number == 1:
1817
+ # MSYS2's supported unattended update procedure invokes Syuu
1818
+ # twice, even when the first pass did not request a restart.
1819
+ reporter.note(
1820
+ "Starting the required second MSYS2 update pass in a new "
1821
+ "process…"
1822
+ )
1823
+ continue
1824
+ return
1825
+ raise RuntimeInstallError(
1826
+ "MSYS2 repeatedly requested a core-runtime restart; the temporary "
1827
+ "base was not published"
1828
+ )
1829
+ finally:
1830
+ _stop_private_gpg_agents(
1831
+ root,
1832
+ env=env,
1833
+ reporter=reporter,
1834
+ runner=runner,
1835
+ strict=sys.exc_info()[0] is None,
1836
+ )
1837
+
1838
+
1839
+ def _stop_private_gpg_agents(
1840
+ root: Path,
1841
+ *,
1842
+ env: Mapping[str, str],
1843
+ reporter: InstallReporter,
1844
+ runner: Runner | None,
1845
+ strict: bool,
1846
+ ) -> None:
1847
+ returncode = _run_checked(
1848
+ _bash_command(root, _PRIVATE_GPG_SHUTDOWN_COMMAND),
1849
+ env=env,
1850
+ reporter=reporter,
1851
+ runner=runner,
1852
+ label="Stopping private MSYS2 keyring agents",
1853
+ allow_failure=True,
1854
+ )
1855
+ if returncode == 0:
1856
+ return
1857
+ if not strict:
1858
+ reporter.note(
1859
+ "Could not stop a private MSYS2 keyring agent while recovering "
1860
+ "from another installation failure.",
1861
+ level="warning",
1862
+ )
1863
+ return
1864
+ reporter.command_failed("Stopping private MSYS2 keyring agents", returncode)
1865
+ raise RuntimeInstallError(
1866
+ "stopping private MSYS2 keyring agents failed with exit code "
1867
+ f"{returncode}"
1732
1868
  )
1733
1869
 
1734
1870
 
@@ -2382,16 +2518,20 @@ def install_managed_runtime(
2382
2518
  "pacman is noninteractive; displayed [Y/n] prompts do "
2383
2519
  "not require input."
2384
2520
  )
2521
+ reporter.note(
2522
+ "Core updates may close the first MSYS2 process; "
2523
+ "Railmux will start the required fresh process "
2524
+ "automatically.",
2525
+ level="muted",
2526
+ )
2385
2527
  package_cache = cache_base / f"pacman-{MSYS2_RUNTIME_ID}"
2386
2528
  package_cache.mkdir(parents=True, exist_ok=True)
2387
- _run_pacman_with_recovery(
2529
+ _run_base_update_with_restarts(
2388
2530
  root,
2389
- packages=False,
2390
2531
  cache=package_cache,
2391
2532
  env=child_env,
2392
2533
  reporter=reporter,
2393
2534
  runner=runner,
2394
- label="MSYS2 base update",
2395
2535
  mirror_optimizer=mirror_optimizer,
2396
2536
  )
2397
2537
  reporter.done()
@@ -2428,16 +2568,25 @@ def install_managed_runtime(
2428
2568
  reporter=reporter,
2429
2569
  runner=runner,
2430
2570
  )
2431
- _run_pacman_with_recovery(
2432
- root,
2433
- packages=True,
2434
- cache=package_cache,
2435
- env=child_env,
2436
- reporter=reporter,
2437
- runner=runner,
2438
- label="MSYS2 package installation",
2439
- mirror_optimizer=mirror_optimizer,
2440
- )
2571
+ try:
2572
+ _run_pacman_with_recovery(
2573
+ root,
2574
+ packages=True,
2575
+ cache=package_cache,
2576
+ env=child_env,
2577
+ reporter=reporter,
2578
+ runner=runner,
2579
+ label="MSYS2 package installation",
2580
+ mirror_optimizer=mirror_optimizer,
2581
+ )
2582
+ finally:
2583
+ _stop_private_gpg_agents(
2584
+ root,
2585
+ env=child_env,
2586
+ reporter=reporter,
2587
+ runner=runner,
2588
+ strict=sys.exc_info()[0] is None,
2589
+ )
2441
2590
  reporter.done()
2442
2591
 
2443
2592
  identity = _ensure_base_content_identity(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev26
3
+ Version: 0.4.0.dev28
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
@@ -174,4 +174,5 @@ tests/test_windows_wrapper_contract.py
174
174
  tests/test_yolo_prompt.py
175
175
  tools/denested_transport_benchmark.py
176
176
  tools/release_notes.py
177
- tools/wheel_smoke.py
177
+ tools/wheel_smoke.py
178
+ tools/windows_archive_smoke.py
@@ -2,8 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  import os
5
+ import subprocess
5
6
  import sys
6
7
 
8
+ import pytest
9
+
10
+ from railmux import windows_msys2
7
11
  from railmux.windows_install_log import InstallReporter, install_log_path
8
12
  from railmux.windows_msys2 import _run_checked
9
13
 
@@ -60,6 +64,22 @@ def test_verbose_reporter_streams_the_same_sanitized_output(tmp_path):
60
64
  assert "package 中文\nprogress\n" in path.read_text(encoding="utf-8")
61
65
 
62
66
 
67
+ def test_reporter_recognizes_only_the_explicit_msys2_core_restart(tmp_path):
68
+ path = tmp_path / "install.log"
69
+ with InstallReporter(path, verbose=False, stream=io.StringIO()) as reporter:
70
+ reporter.command_started("first update")
71
+ reporter.command_output(
72
+ ":: To complete this update all MSYS2 processes including this "
73
+ "terminal will be closed. Confirm to proceed [Y/n]"
74
+ )
75
+ reporter.command_output_finished()
76
+ assert reporter.command_requested_msys2_restart
77
+
78
+ reporter.command_started("second update")
79
+ reporter.command_output("warning: terminate other MSYS2 programs\n")
80
+ assert not reporter.command_requested_msys2_restart
81
+
82
+
63
83
  def test_verbose_reporter_never_fails_on_a_legacy_windows_console_codec(tmp_path):
64
84
  stream = Cp1252Stream()
65
85
  path = tmp_path / "install-0.4.0.dev7-20260803T000000Z-5.log"
@@ -194,6 +214,27 @@ def test_default_command_runner_uses_no_stdin_and_captures_utf8(tmp_path):
194
214
  assert "captured 中文" in path.read_text(encoding="utf-8")
195
215
 
196
216
 
217
+ def test_windows_missing_dll_status_is_explained(tmp_path, monkeypatch):
218
+ stream = io.StringIO()
219
+ path = tmp_path / "install.log"
220
+ monkeypatch.setattr(windows_msys2, "_NATIVE_WINDOWS", True)
221
+
222
+ with InstallReporter(path, verbose=False, stream=stream) as reporter:
223
+ with pytest.raises(windows_msys2.RuntimeInstallError, match="3221225781"):
224
+ _run_checked(
225
+ ["fixture.exe"],
226
+ env={},
227
+ reporter=reporter,
228
+ runner=lambda *_args, **_kwargs: subprocess.CompletedProcess(
229
+ [], 0xC0000135
230
+ ),
231
+ label="MSYS2 base update",
232
+ )
233
+
234
+ assert "STATUS_DLL_NOT_FOUND" in stream.getvalue()
235
+ assert "security software" in path.read_text(encoding="utf-8")
236
+
237
+
197
238
  def test_compact_reporter_surfaces_extraction_and_package_progress(tmp_path):
198
239
  stream = io.StringIO()
199
240
  path = tmp_path / "install-0.4.0.dev8-20260803T000000Z-7.log"
@@ -3,7 +3,9 @@ from __future__ import annotations
3
3
  import hashlib
4
4
  import io
5
5
  import json
6
+ import os
6
7
  import re
8
+ import stat
7
9
  import subprocess
8
10
  import tarfile
9
11
  from contextlib import contextmanager
@@ -795,6 +797,8 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
795
797
  ).read_text(encoding="utf-8")
796
798
  )["railmux"] == VERSION
797
799
  joined = [" ".join(command) for command in commands]
800
+ base_updates = [command for command in joined if "-Syuu --noconfirm" in command]
801
+ assert len(base_updates) == 2
798
802
  assert any("-Syu --noconfirm" in command for command in joined)
799
803
  assert any("--needed tmux python python-pip" in command for command in joined)
800
804
  assert any('python -m venv "$1/venv"' in command for command in joined)
@@ -804,7 +808,8 @@ def test_install_is_staged_and_activated_only_after_exact_probe(tmp_path):
804
808
  assert len(logs) == 1
805
809
  log = logs[0].read_text(encoding="utf-8")
806
810
  assert "[1/7] Preparing verified MSYS2" in log
807
- assert "--- MSYS2 base update ---" in log
811
+ assert "--- MSYS2 base update pass 1 ---" in log
812
+ assert "--- MSYS2 base update pass 2 ---" in log
808
813
  assert "Installation completed successfully" in log
809
814
  config = runtime.root / "etc" / "railmux-pacman.conf"
810
815
  assert "[msys]" in config.read_text(encoding="utf-8")
@@ -1291,12 +1296,16 @@ def test_tar_xz_extraction_is_internal_bounded_and_progress_visible(
1291
1296
  _tar_directory("msys64"),
1292
1297
  _tar_directory("msys64/usr"),
1293
1298
  _tar_file("msys64/usr/bash.exe", b"verified bash"),
1299
+ _tar_file("msys64/usr/read-only-upstream", b"must stay writable"),
1294
1300
  ]
1301
+ entries[-1][0].mode = 0o444
1295
1302
  archive = tmp_path / MSYS2_ARCHIVE_NAME
1296
1303
  _write_test_tar(archive, entries)
1297
- monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_MEMBER_COUNT", 3)
1304
+ monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_MEMBER_COUNT", 4)
1298
1305
  monkeypatch.setattr(
1299
- windows_msys2, "MSYS2_ARCHIVE_UNPACKED_SIZE", len(b"verified bash")
1306
+ windows_msys2,
1307
+ "MSYS2_ARCHIVE_UNPACKED_SIZE",
1308
+ len(b"verified bash") + len(b"must stay writable"),
1300
1309
  )
1301
1310
  destination = tmp_path / "stage"
1302
1311
  destination.mkdir()
@@ -1309,12 +1318,34 @@ def test_tar_xz_extraction_is_internal_bounded_and_progress_visible(
1309
1318
  assert (destination / "msys64" / "usr" / "bash.exe").read_bytes() == (
1310
1319
  b"verified bash"
1311
1320
  )
1321
+ package_owned = destination / "msys64" / "usr" / "read-only-upstream"
1322
+ if os.name == "nt":
1323
+ readonly = getattr(stat, "FILE_ATTRIBUTE_READONLY", 0x1)
1324
+ assert not getattr(package_owned.stat(), "st_file_attributes", 0) & readonly
1325
+ else:
1326
+ assert stat.S_IMODE(package_owned.stat().st_mode) == 0o444
1312
1327
  rendered = output.getvalue()
1313
- assert "Extracting private runtime: 1/3 files (33%)" in rendered
1314
- assert "Extracting private runtime: 3/3 files (100%)" in rendered
1328
+ assert "Extracting private runtime: 1/4 files (25%)" in rendered
1329
+ assert "Extracting private runtime: 4/4 files (100%)" in rendered
1315
1330
  assert "MSYS2 archive extraction" in log.read_text(encoding="utf-8")
1316
1331
 
1317
1332
 
1333
+ def test_native_windows_extraction_clears_posix_read_only_modes(
1334
+ tmp_path, monkeypatch,
1335
+ ):
1336
+ path = tmp_path / "package-owned"
1337
+ path.write_bytes(b"fixture")
1338
+ applied = []
1339
+ monkeypatch.setattr(windows_msys2, "_NATIVE_WINDOWS", True)
1340
+ monkeypatch.setattr(
1341
+ windows_msys2.os, "chmod", lambda target, mode: applied.append((target, mode))
1342
+ )
1343
+
1344
+ windows_msys2._apply_archive_mode(path, 0o444)
1345
+
1346
+ assert applied == [(path, stat.S_IWRITE)]
1347
+
1348
+
1318
1349
  @pytest.mark.parametrize(
1319
1350
  ("member", "message"),
1320
1351
  [
@@ -1444,6 +1475,112 @@ def test_pacman_network_failure_retries_with_cache_and_relaxed_timeout(tmp_path)
1444
1475
  assert f"Server = {repo}" in rendered
1445
1476
 
1446
1477
 
1478
+ def test_base_update_restarts_after_confirmed_core_shutdown(tmp_path):
1479
+ attempts = []
1480
+ restart = (
1481
+ b"upgrading msys2-runtime...\n"
1482
+ b":: To complete this update all MSYS2 processes including this "
1483
+ b"terminal will be closed. Confirm to proceed [Y/n]\n"
1484
+ )
1485
+
1486
+ def runner(argv, *, env, check):
1487
+ attempts.append(argv)
1488
+ update_attempts = [item for item in attempts if "-Syuu" in item[4]]
1489
+ if len(update_attempts) == 1 and "-Syuu" in argv[4]:
1490
+ return completed(argv, returncode=0xC0000135, stdout=restart)
1491
+ return completed(argv, stdout=b"there is nothing to do\n")
1492
+
1493
+ stream = io.StringIO()
1494
+ with InstallReporter(
1495
+ tmp_path / "install.log", verbose=False, stream=stream
1496
+ ) as reporter:
1497
+ windows_msys2._run_base_update_with_restarts(
1498
+ tmp_path / "msys64",
1499
+ cache=tmp_path / "cache",
1500
+ env={},
1501
+ reporter=reporter,
1502
+ runner=runner,
1503
+ mirror_optimizer=lambda _root: PacmanMirrorDecision(
1504
+ None, None, False, (), ()
1505
+ ),
1506
+ )
1507
+
1508
+ updates = [argv for argv in attempts if "-Syuu --noconfirm" in argv[4]]
1509
+ assert len(updates) == 2
1510
+ assert updates[0][0] == updates[1][0]
1511
+ assert windows_msys2._PRIVATE_GPG_SHUTDOWN_COMMAND in attempts[-1][4]
1512
+ rendered = stream.getvalue()
1513
+ assert "closed its own process as expected" in rendered
1514
+ assert "failed with exit code" not in rendered
1515
+
1516
+
1517
+ def test_base_update_does_not_mask_unconfirmed_missing_dll(tmp_path, monkeypatch):
1518
+ monkeypatch.setattr(windows_msys2, "_NATIVE_WINDOWS", True)
1519
+ attempts = []
1520
+
1521
+ def runner(argv, *, env, check):
1522
+ attempts.append(argv)
1523
+ if windows_msys2._PRIVATE_GPG_SHUTDOWN_COMMAND in argv[4]:
1524
+ return completed(argv)
1525
+ return completed(
1526
+ argv,
1527
+ returncode=0xC0000135,
1528
+ stdout=b"upgrading msys2-runtime...\n",
1529
+ )
1530
+
1531
+ stream = io.StringIO()
1532
+ with InstallReporter(
1533
+ tmp_path / "install.log", verbose=False, stream=stream
1534
+ ) as reporter:
1535
+ with pytest.raises(RuntimeInstallError, match="3221225781"):
1536
+ windows_msys2._run_base_update_with_restarts(
1537
+ tmp_path / "msys64",
1538
+ cache=tmp_path / "cache",
1539
+ env={},
1540
+ reporter=reporter,
1541
+ runner=runner,
1542
+ mirror_optimizer=lambda _root: PacmanMirrorDecision(
1543
+ None, None, False, (), ()
1544
+ ),
1545
+ )
1546
+
1547
+ updates = [argv for argv in attempts if "-Syuu --noconfirm" in argv[4]]
1548
+ assert len(updates) == 1
1549
+ assert "outside a confirmed MSYS2 core-update restart" in stream.getvalue()
1550
+
1551
+
1552
+ def test_base_update_restart_loop_is_bounded(tmp_path):
1553
+ attempts = []
1554
+ restart = (
1555
+ b":: To complete this update all MSYS2 processes including this "
1556
+ b"terminal will be closed. Confirm to proceed [Y/n]\n"
1557
+ )
1558
+
1559
+ def runner(argv, *, env, check):
1560
+ attempts.append(argv)
1561
+ if windows_msys2._PRIVATE_GPG_SHUTDOWN_COMMAND in argv[4]:
1562
+ return completed(argv)
1563
+ return completed(argv, stdout=restart)
1564
+
1565
+ with InstallReporter(
1566
+ tmp_path / "install.log", verbose=False, stream=io.StringIO()
1567
+ ) as reporter:
1568
+ with pytest.raises(RuntimeInstallError, match="repeatedly requested"):
1569
+ windows_msys2._run_base_update_with_restarts(
1570
+ tmp_path / "msys64",
1571
+ cache=tmp_path / "cache",
1572
+ env={},
1573
+ reporter=reporter,
1574
+ runner=runner,
1575
+ mirror_optimizer=lambda _root: PacmanMirrorDecision(
1576
+ None, None, False, (), ()
1577
+ ),
1578
+ )
1579
+
1580
+ updates = [argv for argv in attempts if "-Syuu --noconfirm" in argv[4]]
1581
+ assert len(updates) == windows_msys2._MAX_BASE_UPDATE_PASSES
1582
+
1583
+
1447
1584
  def test_completed_package_cache_count_ignores_signatures_partials_and_links(tmp_path):
1448
1585
  (tmp_path / "python.pkg.tar.zst").write_bytes(b"package")
1449
1586
  (tmp_path / "python.pkg.tar.zst.sig").write_bytes(b"signature")