railmux 0.4.0.dev28__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.
- {railmux-0.4.0.dev28/src/railmux.egg-info → railmux-0.4.0.dev29}/PKG-INFO +1 -1
- railmux-0.4.0.dev29/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/windows_install_log.py +21 -4
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/windows_msys2.py +279 -5
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_windows_install_log.py +36 -2
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_windows_msys2.py +264 -3
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tools/windows_archive_smoke.py +37 -1
- railmux-0.4.0.dev28/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/LICENSE +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/README.md +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/pyproject.toml +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/setup.cfg +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/cli.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev28 → railmux-0.4.0.dev29}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev29"
|
|
@@ -60,6 +60,7 @@ _MSYS2_RESTART_MARKER = (
|
|
|
60
60
|
"to complete this update all msys2 processes including this terminal "
|
|
61
61
|
"will be closed"
|
|
62
62
|
)
|
|
63
|
+
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
63
64
|
_EXTRACTION_PERCENT_RE = re.compile(r"(?:^|\s)([0-9]{1,3})%\s")
|
|
64
65
|
_PACKAGE_COUNT_RE = re.compile(r"^Packages \(([0-9]+)\)")
|
|
65
66
|
_PACKAGE_DOWNLOAD_RE = re.compile(r"^\s*([^ ]+) downloading\.\.\.\s*$")
|
|
@@ -91,7 +92,12 @@ def install_log_path(
|
|
|
91
92
|
)
|
|
92
93
|
|
|
93
94
|
|
|
94
|
-
def _prune_install_logs(
|
|
95
|
+
def _prune_install_logs(
|
|
96
|
+
directory: Path,
|
|
97
|
+
*,
|
|
98
|
+
keep: int = _LOG_KEEP_COUNT,
|
|
99
|
+
preserve: Path | None = None,
|
|
100
|
+
) -> None:
|
|
95
101
|
candidates: list[Path] = []
|
|
96
102
|
try:
|
|
97
103
|
entries = list(directory.iterdir())
|
|
@@ -107,7 +113,11 @@ def _prune_install_logs(directory: Path, *, keep: int = _LOG_KEEP_COUNT) -> None
|
|
|
107
113
|
)
|
|
108
114
|
except OSError:
|
|
109
115
|
return
|
|
110
|
-
|
|
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:
|
|
111
121
|
try:
|
|
112
122
|
stale.unlink()
|
|
113
123
|
except OSError:
|
|
@@ -147,12 +157,15 @@ class InstallReporter:
|
|
|
147
157
|
self._pip_network_failure = False
|
|
148
158
|
self._hard_failed_hosts: set[str] = set()
|
|
149
159
|
self._msys2_restart_requested = False
|
|
160
|
+
self._msys2_restart_lines: deque[str] = deque(maxlen=3)
|
|
150
161
|
|
|
151
162
|
def __enter__(self) -> InstallReporter:
|
|
152
163
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
153
164
|
self._log = self.path.open("x", encoding="utf-8", newline="\n")
|
|
154
165
|
self._write_log("Railmux Windows runtime installation\n")
|
|
155
|
-
|
|
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)
|
|
156
169
|
return self
|
|
157
170
|
|
|
158
171
|
def __exit__(self, _exc_type, _exc, _traceback) -> None:
|
|
@@ -215,6 +228,7 @@ class InstallReporter:
|
|
|
215
228
|
self._pip_network_failure = False
|
|
216
229
|
self._hard_failed_hosts.clear()
|
|
217
230
|
self._msys2_restart_requested = False
|
|
231
|
+
self._msys2_restart_lines.clear()
|
|
218
232
|
self._write_log(f"\n--- {label} ---\n")
|
|
219
233
|
|
|
220
234
|
def command_output(self, output: bytes | str | None) -> None:
|
|
@@ -265,7 +279,10 @@ class InstallReporter:
|
|
|
265
279
|
if line.strip():
|
|
266
280
|
self._tail.append(line)
|
|
267
281
|
lowered = line.lower()
|
|
268
|
-
|
|
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):
|
|
269
286
|
self._msys2_restart_requested = True
|
|
270
287
|
if any(marker in lowered for marker in _NETWORK_ERROR_MARKERS):
|
|
271
288
|
self._network_failure = True
|
|
@@ -120,6 +120,28 @@ _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
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
|
+
)
|
|
123
145
|
_PRIVATE_GPG_SHUTDOWN_COMMAND = (
|
|
124
146
|
"gpgconf --homedir /etc/pacman.d/gnupg --kill all"
|
|
125
147
|
)
|
|
@@ -200,6 +222,16 @@ ArchiveProbe = Callable[[str, str, int], _ArchiveProbe]
|
|
|
200
222
|
ArchiveResume = Callable[[str, str, int, int, Callable[[bytes], None]], None]
|
|
201
223
|
|
|
202
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
|
+
|
|
203
235
|
def _format_download_size(size: int) -> str:
|
|
204
236
|
if size >= 1024 * 1024:
|
|
205
237
|
return f"{size / (1024 * 1024):.1f} MiB"
|
|
@@ -1638,9 +1670,11 @@ def _run_checked(
|
|
|
1638
1670
|
def _explain_windows_status(returncode: int, reporter: InstallReporter) -> None:
|
|
1639
1671
|
if _NATIVE_WINDOWS and returncode & 0xFFFFFFFF == 0xC0000135:
|
|
1640
1672
|
reporter.note(
|
|
1641
|
-
"Windows reported STATUS_DLL_NOT_FOUND
|
|
1642
|
-
"
|
|
1643
|
-
"
|
|
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.",
|
|
1644
1678
|
level="error",
|
|
1645
1679
|
)
|
|
1646
1680
|
|
|
@@ -1676,6 +1710,215 @@ def _report_mirror_decision(
|
|
|
1676
1710
|
)
|
|
1677
1711
|
|
|
1678
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
|
+
|
|
1679
1922
|
def _refresh_pacman_mirrors(
|
|
1680
1923
|
root: Path,
|
|
1681
1924
|
*,
|
|
@@ -1705,6 +1948,7 @@ def _run_pacman_with_recovery(
|
|
|
1705
1948
|
mirror_optimizer: MirrorOptimizer,
|
|
1706
1949
|
allow_core_restart: bool = False,
|
|
1707
1950
|
) -> bool:
|
|
1951
|
+
checkpoint = _pacman_log_checkpoint(root) if not packages else None
|
|
1708
1952
|
argv = _bash_command(root, _pacman_command(packages=packages, relaxed=False), str(cache))
|
|
1709
1953
|
returncode = _run_checked(
|
|
1710
1954
|
argv,
|
|
@@ -1715,9 +1959,23 @@ def _run_pacman_with_recovery(
|
|
|
1715
1959
|
progress="pacman",
|
|
1716
1960
|
allow_failure=True,
|
|
1717
1961
|
)
|
|
1718
|
-
restart_requested =
|
|
1962
|
+
restart_requested = (
|
|
1963
|
+
_restart_evidence(root, checkpoint, reporter, returncode)
|
|
1964
|
+
if checkpoint is not None
|
|
1965
|
+
else False
|
|
1966
|
+
)
|
|
1719
1967
|
if returncode == 0:
|
|
1720
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
|
+
)
|
|
1721
1979
|
if allow_core_restart and not packages and restart_requested:
|
|
1722
1980
|
reporter.note(
|
|
1723
1981
|
"MSYS2 replaced core runtime files and closed its own process as "
|
|
@@ -1759,6 +2017,7 @@ def _run_pacman_with_recovery(
|
|
|
1759
2017
|
_pacman_command(packages=packages, relaxed=True),
|
|
1760
2018
|
str(cache),
|
|
1761
2019
|
)
|
|
2020
|
+
retry_checkpoint = _pacman_log_checkpoint(root) if not packages else None
|
|
1762
2021
|
retry_returncode = _run_checked(
|
|
1763
2022
|
retry_argv,
|
|
1764
2023
|
env=env,
|
|
@@ -1768,9 +2027,24 @@ def _run_pacman_with_recovery(
|
|
|
1768
2027
|
progress="pacman",
|
|
1769
2028
|
allow_failure=True,
|
|
1770
2029
|
)
|
|
1771
|
-
restart_requested =
|
|
2030
|
+
restart_requested = (
|
|
2031
|
+
_restart_evidence(root, retry_checkpoint, reporter, retry_returncode)
|
|
2032
|
+
if retry_checkpoint is not None
|
|
2033
|
+
else False
|
|
2034
|
+
)
|
|
1772
2035
|
if retry_returncode == 0:
|
|
1773
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
|
+
)
|
|
1774
2048
|
if allow_core_restart and not packages and restart_requested:
|
|
1775
2049
|
reporter.note(
|
|
1776
2050
|
"MSYS2 replaced core runtime files and closed its own process as "
|
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
from railmux import windows_msys2
|
|
10
|
+
from railmux import windows_install_log, windows_msys2
|
|
11
11
|
from railmux.windows_install_log import InstallReporter, install_log_path
|
|
12
12
|
from railmux.windows_msys2 import _run_checked
|
|
13
13
|
|
|
@@ -80,6 +80,19 @@ def test_reporter_recognizes_only_the_explicit_msys2_core_restart(tmp_path):
|
|
|
80
80
|
assert not reporter.command_requested_msys2_restart
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def test_reporter_recognizes_a_wrapped_msys2_core_restart(tmp_path):
|
|
84
|
+
path = tmp_path / "install.log"
|
|
85
|
+
with InstallReporter(path, verbose=False, stream=io.StringIO()) as reporter:
|
|
86
|
+
reporter.command_started("update")
|
|
87
|
+
reporter.command_output(
|
|
88
|
+
":: To complete this update all MSYS2 processes including\n"
|
|
89
|
+
"\n"
|
|
90
|
+
"this terminal will be closed. Confirm to proceed [Y/n]\n"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
assert reporter.command_requested_msys2_restart
|
|
94
|
+
|
|
95
|
+
|
|
83
96
|
def test_verbose_reporter_never_fails_on_a_legacy_windows_console_codec(tmp_path):
|
|
84
97
|
stream = Cp1252Stream()
|
|
85
98
|
path = tmp_path / "install-0.4.0.dev7-20260803T000000Z-5.log"
|
|
@@ -174,6 +187,25 @@ def test_reporter_retains_only_five_owned_logs(tmp_path):
|
|
|
174
187
|
assert unrelated.read_text(encoding="utf-8") == "keep"
|
|
175
188
|
|
|
176
189
|
|
|
190
|
+
def test_log_retention_never_removes_the_active_oldest_timestamp(tmp_path):
|
|
191
|
+
logs = tmp_path / "logs"
|
|
192
|
+
logs.mkdir()
|
|
193
|
+
current = logs / "install-0.4.0.dev29-20260806T000000Z-99.log"
|
|
194
|
+
current.write_text("active", encoding="utf-8")
|
|
195
|
+
os.utime(current, ns=(1, 1))
|
|
196
|
+
for number in range(6):
|
|
197
|
+
path = logs / f"install-0.4.0.dev28-20260805T00000{number}Z-{number}.log"
|
|
198
|
+
path.write_text(str(number), encoding="utf-8")
|
|
199
|
+
os.utime(path, ns=(number + 10, number + 10))
|
|
200
|
+
|
|
201
|
+
windows_install_log._prune_install_logs(logs, preserve=current)
|
|
202
|
+
|
|
203
|
+
owned = sorted(logs.glob("install-*.log"))
|
|
204
|
+
assert len(owned) == 5
|
|
205
|
+
assert current in owned
|
|
206
|
+
assert current.read_text(encoding="utf-8") == "active"
|
|
207
|
+
|
|
208
|
+
|
|
177
209
|
def test_log_path_is_private_versioned_and_utc(tmp_path, monkeypatch):
|
|
178
210
|
monkeypatch.setattr(os, "getpid", lambda: 42)
|
|
179
211
|
|
|
@@ -232,7 +264,9 @@ def test_windows_missing_dll_status_is_explained(tmp_path, monkeypatch):
|
|
|
232
264
|
)
|
|
233
265
|
|
|
234
266
|
assert "STATUS_DLL_NOT_FOUND" in stream.getvalue()
|
|
235
|
-
assert "
|
|
267
|
+
assert "matching package-database version change" in path.read_text(
|
|
268
|
+
encoding="utf-8"
|
|
269
|
+
)
|
|
236
270
|
|
|
237
271
|
|
|
238
272
|
def test_compact_reporter_surfaces_extraction_and_package_progress(tmp_path):
|