railmux 0.4.0.dev29__tar.gz → 0.4.0.dev31__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.dev29/src/railmux.egg-info → railmux-0.4.0.dev31}/PKG-INFO +1 -1
- railmux-0.4.0.dev31/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/cli.py +27 -1
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/windows_install_log.py +13 -6
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/windows_msys2.py +38 -9
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/windows_pacman.py +24 -3
- railmux-0.4.0.dev31/src/railmux/windows_paths.py +73 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_cli.py +75 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_windows_install_log.py +20 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_windows_msys2.py +67 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_windows_pacman.py +35 -0
- railmux-0.4.0.dev31/tests/test_windows_paths.py +83 -0
- railmux-0.4.0.dev29/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/LICENSE +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/README.md +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/pyproject.toml +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/setup.cfg +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.0.dev29 → railmux-0.4.0.dev31}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev31"
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import os
|
|
5
|
+
import signal
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
@@ -109,6 +110,9 @@ def _run_tmux_client_with_watchdog(
|
|
|
109
110
|
attributes = termios.tcgetattr(sys.stdin.fileno())
|
|
110
111
|
except (OSError, termios.error):
|
|
111
112
|
pass
|
|
113
|
+
direct_terminal_size = (
|
|
114
|
+
_interactive_terminal_size() if managed_windows else None
|
|
115
|
+
)
|
|
112
116
|
started_at = time.monotonic()
|
|
113
117
|
try:
|
|
114
118
|
popen_kwargs: dict[str, object] = {"env": env}
|
|
@@ -127,7 +131,7 @@ def _run_tmux_client_with_watchdog(
|
|
|
127
131
|
*,
|
|
128
132
|
asynchronous_probe: bool = False,
|
|
129
133
|
) -> tuple[int, bool]:
|
|
130
|
-
nonlocal expected_target, expected_session_id
|
|
134
|
+
nonlocal direct_terminal_size, expected_target, expected_session_id
|
|
131
135
|
watchdog = tmux_health.FailureWatchdog.starting(
|
|
132
136
|
(
|
|
133
137
|
time.monotonic()
|
|
@@ -151,6 +155,28 @@ def _run_tmux_client_with_watchdog(
|
|
|
151
155
|
pump = getattr(process, "pump", None)
|
|
152
156
|
if pump is None:
|
|
153
157
|
time.sleep(0.25)
|
|
158
|
+
if managed_windows:
|
|
159
|
+
current_size = _interactive_terminal_size()
|
|
160
|
+
if (
|
|
161
|
+
current_size is not None
|
|
162
|
+
and current_size != direct_terminal_size
|
|
163
|
+
):
|
|
164
|
+
direct_terminal_size = current_size
|
|
165
|
+
pid = getattr(process, "pid", None)
|
|
166
|
+
if (
|
|
167
|
+
isinstance(pid, int)
|
|
168
|
+
and not isinstance(pid, bool)
|
|
169
|
+
and pid > 0
|
|
170
|
+
):
|
|
171
|
+
try:
|
|
172
|
+
# MSYS2 occasionally misses the native
|
|
173
|
+
# console's resize notification. Signal only
|
|
174
|
+
# this launcher's direct tmux client so it
|
|
175
|
+
# re-reads both dimensions; tmux remains the
|
|
176
|
+
# authority for shared-window sizing.
|
|
177
|
+
os.kill(pid, signal.SIGWINCH)
|
|
178
|
+
except (OSError, ValueError):
|
|
179
|
+
pass
|
|
154
180
|
else:
|
|
155
181
|
pump(0.25)
|
|
156
182
|
now = time.monotonic()
|
|
@@ -22,6 +22,7 @@ from railmux.terminal_status import (
|
|
|
22
22
|
STYLE_WARNING,
|
|
23
23
|
styled,
|
|
24
24
|
)
|
|
25
|
+
from railmux.windows_paths import managed_windows_data_root
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
_LOG_KEEP_COUNT = 5
|
|
@@ -79,14 +80,14 @@ def install_log_path(
|
|
|
79
80
|
*,
|
|
80
81
|
version: str,
|
|
81
82
|
clock: Callable[[], float] = time.time,
|
|
83
|
+
data_root: Path | None = None,
|
|
82
84
|
) -> Path:
|
|
83
|
-
|
|
84
|
-
if
|
|
85
|
-
raise OSError("
|
|
85
|
+
root = managed_windows_data_root(environ) if data_root is None else data_root
|
|
86
|
+
if root is None:
|
|
87
|
+
raise OSError("a non-virtualized Windows data directory is unavailable")
|
|
86
88
|
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime(clock()))
|
|
87
89
|
return (
|
|
88
|
-
|
|
89
|
-
/ "Railmux"
|
|
90
|
+
root
|
|
90
91
|
/ "logs"
|
|
91
92
|
/ f"install-{version}-{stamp}-{os.getpid()}.log"
|
|
92
93
|
)
|
|
@@ -323,7 +324,13 @@ class InstallReporter:
|
|
|
323
324
|
if package_count is not None:
|
|
324
325
|
self._package_total = int(package_count.group(1))
|
|
325
326
|
self._package_step = max(1, self._package_total // 10)
|
|
326
|
-
self._progress_detail =
|
|
327
|
+
self._progress_detail = (
|
|
328
|
+
f"waiting for the first transfer among "
|
|
329
|
+
f"{self._package_total} packages"
|
|
330
|
+
)
|
|
331
|
+
self.note(
|
|
332
|
+
f"Package transaction resolved: {self._package_total} packages."
|
|
333
|
+
)
|
|
327
334
|
return
|
|
328
335
|
total_download = _TOTAL_DOWNLOAD_RE.match(line)
|
|
329
336
|
if total_download is not None:
|
|
@@ -42,6 +42,10 @@ from railmux.windows_pacman import (
|
|
|
42
42
|
validate_transaction_package_mirrors,
|
|
43
43
|
write_msys_only_pacman_config,
|
|
44
44
|
)
|
|
45
|
+
from railmux.windows_paths import (
|
|
46
|
+
legacy_local_app_data_root,
|
|
47
|
+
managed_windows_data_root,
|
|
48
|
+
)
|
|
45
49
|
from railmux.terminal_status import STYLE_ACCENT, STYLE_MUTED, styled
|
|
46
50
|
|
|
47
51
|
|
|
@@ -554,17 +558,17 @@ class Msys2Runtime:
|
|
|
554
558
|
|
|
555
559
|
|
|
556
560
|
def _managed_base(environ: Mapping[str, str]) -> Path | None:
|
|
557
|
-
|
|
558
|
-
if
|
|
561
|
+
data_root = managed_windows_data_root(environ)
|
|
562
|
+
if data_root is None:
|
|
559
563
|
return None
|
|
560
|
-
return
|
|
564
|
+
return data_root / "runtimes"
|
|
561
565
|
|
|
562
566
|
|
|
563
567
|
def _managed_cache(environ: Mapping[str, str]) -> Path | None:
|
|
564
|
-
|
|
565
|
-
if
|
|
568
|
+
data_root = managed_windows_data_root(environ)
|
|
569
|
+
if data_root is None:
|
|
566
570
|
return None
|
|
567
|
-
return
|
|
571
|
+
return data_root / "cache"
|
|
568
572
|
|
|
569
573
|
|
|
570
574
|
def _path_is_link_or_reparse(path: Path) -> bool:
|
|
@@ -595,6 +599,7 @@ def _prepare_cached_archive(
|
|
|
595
599
|
cache: Path,
|
|
596
600
|
*,
|
|
597
601
|
downloader: Downloader | None,
|
|
602
|
+
prior_caches: Sequence[Path] = (),
|
|
598
603
|
) -> tuple[Path, str]:
|
|
599
604
|
cache.mkdir(parents=True, exist_ok=True)
|
|
600
605
|
archive = cache / MSYS2_ARCHIVE_NAME
|
|
@@ -604,6 +609,18 @@ def _prepare_cached_archive(
|
|
|
604
609
|
archive.unlink(missing_ok=True)
|
|
605
610
|
except OSError as exc:
|
|
606
611
|
raise RuntimeInstallError("could not replace the private archive cache") from exc
|
|
612
|
+
for prior_cache in prior_caches:
|
|
613
|
+
prior_archive = prior_cache / MSYS2_ARCHIVE_NAME
|
|
614
|
+
if prior_archive == archive or not _archive_cache_is_valid(prior_archive):
|
|
615
|
+
continue
|
|
616
|
+
try:
|
|
617
|
+
shutil.copyfile(prior_archive, archive)
|
|
618
|
+
except OSError:
|
|
619
|
+
archive.unlink(missing_ok=True)
|
|
620
|
+
continue
|
|
621
|
+
if _archive_cache_is_valid(archive):
|
|
622
|
+
return archive, "verified prior local cache"
|
|
623
|
+
archive.unlink(missing_ok=True)
|
|
607
624
|
source = download_from_sources(
|
|
608
625
|
archive,
|
|
609
626
|
MSYS2_ARCHIVE_SHA256,
|
|
@@ -1178,7 +1195,9 @@ def apply_managed_runtime_prune(
|
|
|
1178
1195
|
"""Re-plan under the install lock, then remove only unchanged candidates."""
|
|
1179
1196
|
base = _managed_base(environ)
|
|
1180
1197
|
if base is None:
|
|
1181
|
-
raise RuntimeInstallError(
|
|
1198
|
+
raise RuntimeInstallError(
|
|
1199
|
+
"a non-virtualized Windows user data directory is unavailable"
|
|
1200
|
+
)
|
|
1182
1201
|
effective_lock = install_lock if lock_factory is None else lock_factory
|
|
1183
1202
|
with effective_lock(base):
|
|
1184
1203
|
current = plan_managed_runtime_prune(
|
|
@@ -2629,11 +2648,16 @@ def install_managed_runtime(
|
|
|
2629
2648
|
base = _managed_base(environ)
|
|
2630
2649
|
cache_base = _managed_cache(environ)
|
|
2631
2650
|
if base is None or cache_base is None:
|
|
2632
|
-
raise RuntimeInstallError(
|
|
2651
|
+
raise RuntimeInstallError(
|
|
2652
|
+
"a non-virtualized Windows user data directory is unavailable"
|
|
2653
|
+
)
|
|
2654
|
+
data_root = base.parent
|
|
2633
2655
|
try:
|
|
2634
2656
|
base.mkdir(parents=True, exist_ok=True)
|
|
2635
2657
|
cache_base.mkdir(parents=True, exist_ok=True)
|
|
2636
|
-
log_path = install_log_path(
|
|
2658
|
+
log_path = install_log_path(
|
|
2659
|
+
environ, version=version, data_root=data_root
|
|
2660
|
+
)
|
|
2637
2661
|
except OSError as exc:
|
|
2638
2662
|
raise RuntimeInstallError("could not create the private runtime log") from exc
|
|
2639
2663
|
final_root = base / "shared" / MSYS2_RUNTIME_ID
|
|
@@ -2749,6 +2773,11 @@ def install_managed_runtime(
|
|
|
2749
2773
|
archive, source = _prepare_cached_archive(
|
|
2750
2774
|
cache_base,
|
|
2751
2775
|
downloader=downloader,
|
|
2776
|
+
prior_caches=tuple(
|
|
2777
|
+
legacy / "cache"
|
|
2778
|
+
for legacy in (legacy_local_app_data_root(environ),)
|
|
2779
|
+
if legacy is not None and legacy != data_root
|
|
2780
|
+
),
|
|
2752
2781
|
)
|
|
2753
2782
|
reporter.done(source)
|
|
2754
2783
|
|
|
@@ -30,7 +30,9 @@ _MIRRORLIST_LIMIT = 256 * 1024
|
|
|
30
30
|
_PACMAN_CONF_LIMIT = 256 * 1024
|
|
31
31
|
_PROBE_BYTES = 256 * 1024
|
|
32
32
|
_PROBE_TIMEOUT = 8.0
|
|
33
|
+
_PACKAGE_PROBE_BYTES = 256 * 1024
|
|
33
34
|
_PACKAGE_PROBE_TIMEOUT = 5.0
|
|
35
|
+
_PACKAGE_PROBE_MIN_RATE = 128 * 1024
|
|
34
36
|
_SWITCH_RATIO = 1.25
|
|
35
37
|
_FRESHNESS_WINDOW_SECONDS = 6 * 60 * 60
|
|
36
38
|
_ZSTD_MAGIC = b"\x28\xb5\x2f\xfd"
|
|
@@ -91,18 +93,21 @@ def _probe_package_url(
|
|
|
91
93
|
url: str,
|
|
92
94
|
*,
|
|
93
95
|
opener: Callable[..., object] = urllib.request.urlopen,
|
|
96
|
+
clock: Callable[[], float] = time.monotonic,
|
|
94
97
|
) -> None:
|
|
95
|
-
"""Require
|
|
98
|
+
"""Require a responsive real package path, not only database access."""
|
|
96
99
|
if urllib.parse.urlsplit(url).scheme.lower() != "https":
|
|
97
100
|
raise PacmanMirrorError("package URL is not HTTPS")
|
|
98
101
|
request = urllib.request.Request(
|
|
99
102
|
url,
|
|
100
103
|
headers={
|
|
101
104
|
"Accept-Encoding": "identity",
|
|
102
|
-
"Range": "bytes=0-
|
|
105
|
+
"Range": f"bytes=0-{_PACKAGE_PROBE_BYTES - 1}",
|
|
103
106
|
"User-Agent": "Railmux-pacman-package-probe/1",
|
|
104
107
|
},
|
|
105
108
|
)
|
|
109
|
+
started = clock()
|
|
110
|
+
data = bytearray()
|
|
106
111
|
try:
|
|
107
112
|
with opener(request, timeout=_PACKAGE_PROBE_TIMEOUT) as response:
|
|
108
113
|
final_url = getattr(response, "geturl", lambda: request.full_url)()
|
|
@@ -111,15 +116,31 @@ def _probe_package_url(
|
|
|
111
116
|
status = getattr(response, "status", 200)
|
|
112
117
|
if status not in (200, 206):
|
|
113
118
|
raise PacmanMirrorError(f"package returned HTTP {status}")
|
|
114
|
-
data
|
|
119
|
+
while len(data) < _PACKAGE_PROBE_BYTES:
|
|
120
|
+
chunk = response.read(min(
|
|
121
|
+
64 * 1024, _PACKAGE_PROBE_BYTES - len(data)
|
|
122
|
+
))
|
|
123
|
+
if not chunk:
|
|
124
|
+
break
|
|
125
|
+
data.extend(chunk)
|
|
126
|
+
if clock() - started >= _PACKAGE_PROBE_TIMEOUT:
|
|
127
|
+
break
|
|
115
128
|
except PacmanMirrorError:
|
|
116
129
|
raise
|
|
117
130
|
except urllib.error.HTTPError as exc:
|
|
118
131
|
raise PacmanMirrorError(f"package returned HTTP {exc.code}") from exc
|
|
119
132
|
except (OSError, urllib.error.URLError, http.client.HTTPException) as exc:
|
|
120
133
|
raise PacmanMirrorError("package probe failed") from exc
|
|
134
|
+
elapsed = max(clock() - started, 0.001)
|
|
135
|
+
if len(data) < 1024:
|
|
136
|
+
raise PacmanMirrorError("mirror returned too little package data")
|
|
121
137
|
if not data.startswith(_ZSTD_MAGIC):
|
|
122
138
|
raise PacmanMirrorError("mirror did not return an MSYS package")
|
|
139
|
+
if (
|
|
140
|
+
elapsed >= _PACKAGE_PROBE_TIMEOUT
|
|
141
|
+
or len(data) / elapsed < _PACKAGE_PROBE_MIN_RATE
|
|
142
|
+
):
|
|
143
|
+
raise PacmanMirrorError("package transfer was too slow")
|
|
123
144
|
|
|
124
145
|
|
|
125
146
|
def probe_pacman_mirror(
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Stable native-Windows storage paths for the managed runtime.
|
|
2
|
+
|
|
3
|
+
Microsoft Store and other MSIX-packaged Python interpreters can virtualize new
|
|
4
|
+
writes beneath AppData. Railmux's runtime contains executables that must be
|
|
5
|
+
opened by child processes outside that virtualized view, so packaged Python
|
|
6
|
+
uses a user-profile directory instead.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ctypes
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_ERROR_INSUFFICIENT_BUFFER = 122
|
|
19
|
+
_APPMODEL_ERROR_NO_PACKAGE = 15700
|
|
20
|
+
_PACKAGED_PROFILE_DIRECTORY = ".railmux"
|
|
21
|
+
_PACKAGED_WINDOWS_DIRECTORY = "windows"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _has_windows_package_identity() -> bool:
|
|
25
|
+
"""Return whether this process has an MSIX/AppX package identity."""
|
|
26
|
+
if os.name != "nt":
|
|
27
|
+
return False
|
|
28
|
+
try:
|
|
29
|
+
get_family = ctypes.windll.kernel32.GetCurrentPackageFamilyName
|
|
30
|
+
length = ctypes.c_uint32(0)
|
|
31
|
+
result = int(get_family(ctypes.byref(length), None))
|
|
32
|
+
except (AttributeError, OSError, TypeError, ValueError):
|
|
33
|
+
return _looks_like_packaged_executable(sys.executable)
|
|
34
|
+
if result == _APPMODEL_ERROR_NO_PACKAGE:
|
|
35
|
+
return False
|
|
36
|
+
if result == _ERROR_INSUFFICIENT_BUFFER and length.value > 0:
|
|
37
|
+
return True
|
|
38
|
+
# A Store Python launcher and interpreter both have characteristic physical
|
|
39
|
+
# paths. Keep this fallback fail-safe if the package API is unavailable or
|
|
40
|
+
# returns an unexpected platform error.
|
|
41
|
+
return _looks_like_packaged_executable(sys.executable)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _looks_like_packaged_executable(executable: str) -> bool:
|
|
45
|
+
normalized = executable.replace("/", "\\").casefold()
|
|
46
|
+
return "\\windowsapps\\" in normalized or (
|
|
47
|
+
"\\appdata\\local\\packages\\" in normalized and "\\localcache\\" in normalized
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def managed_windows_data_root(environ: Mapping[str, str]) -> Path | None:
|
|
52
|
+
"""Select one data root visible to both Python and MSYS2 child processes."""
|
|
53
|
+
if _has_windows_package_identity():
|
|
54
|
+
user_profile = environ.get("USERPROFILE", "").strip()
|
|
55
|
+
if not user_profile:
|
|
56
|
+
return None
|
|
57
|
+
return (
|
|
58
|
+
Path(user_profile)
|
|
59
|
+
/ _PACKAGED_PROFILE_DIRECTORY
|
|
60
|
+
/ _PACKAGED_WINDOWS_DIRECTORY
|
|
61
|
+
)
|
|
62
|
+
local_app_data = environ.get("LOCALAPPDATA", "").strip()
|
|
63
|
+
if not local_app_data:
|
|
64
|
+
return None
|
|
65
|
+
return Path(local_app_data) / "Railmux"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def legacy_local_app_data_root(environ: Mapping[str, str]) -> Path | None:
|
|
69
|
+
"""Return the pre-packaged-Python-fix location for read-only cache reuse."""
|
|
70
|
+
local_app_data = environ.get("LOCALAPPDATA", "").strip()
|
|
71
|
+
if not local_app_data:
|
|
72
|
+
return None
|
|
73
|
+
return Path(local_app_data) / "Railmux"
|
|
@@ -63,6 +63,7 @@ src/railmux/windows_bootstrap.py
|
|
|
63
63
|
src/railmux/windows_install_log.py
|
|
64
64
|
src/railmux/windows_msys2.py
|
|
65
65
|
src/railmux/windows_pacman.py
|
|
66
|
+
src/railmux/windows_paths.py
|
|
66
67
|
src/railmux/windows_tmux_lifecycle.py
|
|
67
68
|
src/railmux/windows_ui_transition.py
|
|
68
69
|
src/railmux.egg-info/PKG-INFO
|
|
@@ -168,6 +169,7 @@ tests/test_windows_bootstrap.py
|
|
|
168
169
|
tests/test_windows_install_log.py
|
|
169
170
|
tests/test_windows_msys2.py
|
|
170
171
|
tests/test_windows_pacman.py
|
|
172
|
+
tests/test_windows_paths.py
|
|
171
173
|
tests/test_windows_tmux_lifecycle.py
|
|
172
174
|
tests/test_windows_ui_transition.py
|
|
173
175
|
tests/test_windows_wrapper_contract.py
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import signal
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
4
5
|
from unittest.mock import MagicMock, patch
|
|
@@ -632,6 +633,80 @@ def test_local_tmux_watchdog_exits_and_records_after_consecutive_failures(
|
|
|
632
633
|
assert "stopped responding" in capsys.readouterr().err
|
|
633
634
|
|
|
634
635
|
|
|
636
|
+
def test_managed_windows_direct_attach_notifies_height_only_resize(monkeypatch):
|
|
637
|
+
target = TmuxServerTarget("/tmp/railmux", 77)
|
|
638
|
+
|
|
639
|
+
class DirectClient:
|
|
640
|
+
pid = 321
|
|
641
|
+
returncode = None
|
|
642
|
+
|
|
643
|
+
def poll(self):
|
|
644
|
+
return self.returncode
|
|
645
|
+
|
|
646
|
+
client = DirectClient()
|
|
647
|
+
monkeypatch.setattr(
|
|
648
|
+
"railmux.cli.subprocess.Popen", lambda *_args, **_kwargs: client)
|
|
649
|
+
monkeypatch.setattr("railmux.cli.sys.stdin.isatty", lambda: True)
|
|
650
|
+
monkeypatch.setattr("railmux.cli.sys.stdout.isatty", lambda: True)
|
|
651
|
+
monkeypatch.setattr("railmux.cli.termios.tcgetattr", lambda _fd: ["saved"])
|
|
652
|
+
sizes = [os.terminal_size((120, 40)), os.terminal_size((120, 55))]
|
|
653
|
+
monkeypatch.setattr(
|
|
654
|
+
"railmux.cli.os.get_terminal_size",
|
|
655
|
+
lambda _fd: sizes.pop(0) if len(sizes) > 1 else sizes[0],
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
def finish_after_poll(_seconds):
|
|
659
|
+
client.returncode = 0
|
|
660
|
+
|
|
661
|
+
monkeypatch.setattr("railmux.cli.time.sleep", finish_after_poll)
|
|
662
|
+
monkeypatch.setattr(
|
|
663
|
+
"railmux.provider_paths.running_in_managed_windows_wrapper",
|
|
664
|
+
lambda: True,
|
|
665
|
+
)
|
|
666
|
+
notify = MagicMock()
|
|
667
|
+
monkeypatch.setattr("railmux.cli.os.kill", notify)
|
|
668
|
+
|
|
669
|
+
assert _run_tmux_client_with_watchdog(
|
|
670
|
+
["tmux", "-L", "railmux"], {}, expected_target=target,
|
|
671
|
+
expected_session_id="$7",
|
|
672
|
+
) == 0
|
|
673
|
+
notify.assert_called_once_with(321, signal.SIGWINCH)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def test_posix_direct_attach_does_not_poll_or_signal_resize(monkeypatch):
|
|
677
|
+
class DirectClient:
|
|
678
|
+
pid = 321
|
|
679
|
+
returncode = None
|
|
680
|
+
|
|
681
|
+
def poll(self):
|
|
682
|
+
return self.returncode
|
|
683
|
+
|
|
684
|
+
client = DirectClient()
|
|
685
|
+
monkeypatch.setattr(
|
|
686
|
+
"railmux.cli.subprocess.Popen", lambda *_args, **_kwargs: client)
|
|
687
|
+
monkeypatch.setattr("railmux.cli.sys.stdin.isatty", lambda: True)
|
|
688
|
+
monkeypatch.setattr("railmux.cli.termios.tcgetattr", lambda _fd: ["saved"])
|
|
689
|
+
|
|
690
|
+
def finish_after_poll(_seconds):
|
|
691
|
+
client.returncode = 0
|
|
692
|
+
|
|
693
|
+
monkeypatch.setattr("railmux.cli.time.sleep", finish_after_poll)
|
|
694
|
+
monkeypatch.setattr(
|
|
695
|
+
"railmux.provider_paths.running_in_managed_windows_wrapper",
|
|
696
|
+
lambda: False,
|
|
697
|
+
)
|
|
698
|
+
get_size = MagicMock()
|
|
699
|
+
notify = MagicMock()
|
|
700
|
+
monkeypatch.setattr("railmux.cli.os.get_terminal_size", get_size)
|
|
701
|
+
monkeypatch.setattr("railmux.cli.os.kill", notify)
|
|
702
|
+
|
|
703
|
+
assert _run_tmux_client_with_watchdog(
|
|
704
|
+
["tmux", "-L", "railmux"], {},
|
|
705
|
+
) == 0
|
|
706
|
+
get_size.assert_not_called()
|
|
707
|
+
notify.assert_not_called()
|
|
708
|
+
|
|
709
|
+
|
|
635
710
|
def test_abrupt_tmux_client_exit_records_disappeared_server(monkeypatch):
|
|
636
711
|
target = TmuxServerTarget("/tmp/railmux", 77)
|
|
637
712
|
|
|
@@ -223,6 +223,25 @@ def test_log_path_is_private_versioned_and_utc(tmp_path, monkeypatch):
|
|
|
223
223
|
)
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
def test_log_path_uses_packaged_python_non_virtualized_root(tmp_path, monkeypatch):
|
|
227
|
+
monkeypatch.setattr(
|
|
228
|
+
windows_install_log,
|
|
229
|
+
"managed_windows_data_root",
|
|
230
|
+
lambda _environ: tmp_path / ".railmux" / "windows",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
path = install_log_path(
|
|
234
|
+
{
|
|
235
|
+
"LOCALAPPDATA": str(tmp_path / "AppData" / "Local"),
|
|
236
|
+
"USERPROFILE": str(tmp_path),
|
|
237
|
+
},
|
|
238
|
+
version="0.4.0.dev30",
|
|
239
|
+
clock=lambda: 0,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
assert path.parent == tmp_path / ".railmux" / "windows" / "logs"
|
|
243
|
+
|
|
244
|
+
|
|
226
245
|
def test_default_command_runner_uses_no_stdin_and_captures_utf8(tmp_path):
|
|
227
246
|
stream = io.StringIO()
|
|
228
247
|
path = tmp_path / "install-0.4.0.dev7-20260803T000000Z-4.log"
|
|
@@ -291,6 +310,7 @@ def test_compact_reporter_surfaces_extraction_and_package_progress(tmp_path):
|
|
|
291
310
|
assert "Extracting private runtime: 42%" in rendered
|
|
292
311
|
assert "Extracting private runtime: 100%" in rendered
|
|
293
312
|
assert "Package transaction: 48 packages, 57.40 MiB download." in rendered
|
|
313
|
+
assert "Package transaction resolved: 48 packages." in rendered
|
|
294
314
|
assert "Downloading packages: 1/48" in rendered
|
|
295
315
|
assert "Verifying downloaded package signatures" in rendered
|
|
296
316
|
assert "Installing packages: 1/48" in rendered
|
|
@@ -628,6 +628,22 @@ def test_preview_versions_share_one_base_but_use_separate_app_layers(tmp_path):
|
|
|
628
628
|
)
|
|
629
629
|
|
|
630
630
|
|
|
631
|
+
def test_packaged_python_runtime_root_avoids_local_app_data(tmp_path, monkeypatch):
|
|
632
|
+
safe = tmp_path / ".railmux" / "windows"
|
|
633
|
+
monkeypatch.setattr(
|
|
634
|
+
windows_msys2, "managed_windows_data_root", lambda _environ: safe
|
|
635
|
+
)
|
|
636
|
+
environ = {
|
|
637
|
+
"LOCALAPPDATA": str(tmp_path / "AppData" / "Local"),
|
|
638
|
+
"USERPROFILE": str(tmp_path),
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
assert managed_root(environ) == (
|
|
642
|
+
safe / "runtimes" / "shared" / windows_msys2.MSYS2_RUNTIME_ID
|
|
643
|
+
)
|
|
644
|
+
assert windows_msys2._managed_cache(environ) == safe / "cache"
|
|
645
|
+
|
|
646
|
+
|
|
631
647
|
def test_explicit_user_runtime_is_probed_but_never_requires_managed_marker(tmp_path):
|
|
632
648
|
root = tmp_path / "用户-owned-msys"
|
|
633
649
|
runtime = make_runtime(root, managed=False)
|
|
@@ -1268,6 +1284,57 @@ def test_verified_base_archive_cache_avoids_a_second_download(tmp_path, monkeypa
|
|
|
1268
1284
|
assert source == "verified local cache"
|
|
1269
1285
|
|
|
1270
1286
|
|
|
1287
|
+
def test_packaged_python_reuses_verified_pre_fix_archive_cache(tmp_path, monkeypatch):
|
|
1288
|
+
payload = b"verified prior cache archive"
|
|
1289
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_SIZE", len(payload))
|
|
1290
|
+
monkeypatch.setattr(
|
|
1291
|
+
windows_msys2,
|
|
1292
|
+
"MSYS2_ARCHIVE_SHA256",
|
|
1293
|
+
hashlib.sha256(payload).hexdigest(),
|
|
1294
|
+
)
|
|
1295
|
+
prior = tmp_path / "virtualized-cache"
|
|
1296
|
+
prior.mkdir()
|
|
1297
|
+
(prior / MSYS2_ARCHIVE_NAME).write_bytes(payload)
|
|
1298
|
+
current = tmp_path / "non-virtualized-cache"
|
|
1299
|
+
|
|
1300
|
+
selected, source = windows_msys2._prepare_cached_archive(
|
|
1301
|
+
current,
|
|
1302
|
+
downloader=lambda *_args: pytest.fail("prior cache should avoid downloading"),
|
|
1303
|
+
prior_caches=(prior,),
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
assert selected.read_bytes() == payload
|
|
1307
|
+
assert source == "verified prior local cache"
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def test_packaged_python_never_reuses_unverified_pre_fix_archive(
|
|
1311
|
+
tmp_path, monkeypatch
|
|
1312
|
+
):
|
|
1313
|
+
payload = b"expected archive"
|
|
1314
|
+
monkeypatch.setattr(windows_msys2, "MSYS2_ARCHIVE_SIZE", len(payload))
|
|
1315
|
+
monkeypatch.setattr(
|
|
1316
|
+
windows_msys2,
|
|
1317
|
+
"MSYS2_ARCHIVE_SHA256",
|
|
1318
|
+
hashlib.sha256(payload).hexdigest(),
|
|
1319
|
+
)
|
|
1320
|
+
prior = tmp_path / "virtualized-cache"
|
|
1321
|
+
prior.mkdir()
|
|
1322
|
+
(prior / MSYS2_ARCHIVE_NAME).write_bytes(b"tampered archive")
|
|
1323
|
+
current = tmp_path / "non-virtualized-cache"
|
|
1324
|
+
|
|
1325
|
+
def downloader(_url, destination, _sha256):
|
|
1326
|
+
destination.write_bytes(payload)
|
|
1327
|
+
|
|
1328
|
+
selected, source = windows_msys2._prepare_cached_archive(
|
|
1329
|
+
current,
|
|
1330
|
+
downloader=downloader,
|
|
1331
|
+
prior_caches=(prior,),
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
assert selected.read_bytes() == payload
|
|
1335
|
+
assert source in {label for label, _url in MSYS2_ARCHIVE_SOURCES}
|
|
1336
|
+
|
|
1337
|
+
|
|
1271
1338
|
def _write_test_tar(path: Path, entries: list[tuple[tarfile.TarInfo, bytes]]) -> None:
|
|
1272
1339
|
with tarfile.open(path, mode="w:xz") as archive:
|
|
1273
1340
|
for member, payload in entries:
|
|
@@ -10,6 +10,7 @@ from railmux.windows_pacman import (
|
|
|
10
10
|
PACMAN_MIRROR_SOURCES,
|
|
11
11
|
PacmanMirrorError,
|
|
12
12
|
PacmanMirrorProbe,
|
|
13
|
+
_probe_package_url,
|
|
13
14
|
deactivate_pacman_hosts,
|
|
14
15
|
optimize_pacman_mirror,
|
|
15
16
|
probe_pacman_mirror,
|
|
@@ -231,6 +232,40 @@ def test_network_probe_is_bounded_and_rejects_an_https_downgrade():
|
|
|
231
232
|
assert requests[0][1] == 8.0
|
|
232
233
|
|
|
233
234
|
|
|
235
|
+
def test_package_probe_measures_a_real_bounded_transfer():
|
|
236
|
+
requests = []
|
|
237
|
+
|
|
238
|
+
def opener(request, *, timeout):
|
|
239
|
+
requests.append((request, timeout))
|
|
240
|
+
return FakeResponse(
|
|
241
|
+
b"\x28\xb5\x2f\xfd" + b"x" * (256 * 1024),
|
|
242
|
+
final_url="https://mirror.invalid/python.pkg.tar.zst",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
_probe_package_url(
|
|
246
|
+
"https://mirror.invalid/python.pkg.tar.zst",
|
|
247
|
+
opener=opener,
|
|
248
|
+
clock=lambda: 1.0,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
assert requests[0][0].get_header("Range") == "bytes=0-262143"
|
|
252
|
+
assert requests[0][1] == 5.0
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_package_probe_rejects_a_working_but_slow_source():
|
|
256
|
+
times = [0.0, 3.0]
|
|
257
|
+
|
|
258
|
+
with pytest.raises(PacmanMirrorError, match="too slow"):
|
|
259
|
+
_probe_package_url(
|
|
260
|
+
"https://mirror.invalid/python.pkg.tar.zst",
|
|
261
|
+
opener=lambda _request, **_kwargs: FakeResponse(
|
|
262
|
+
b"\x28\xb5\x2f\xfd" + b"x" * (256 * 1024),
|
|
263
|
+
final_url="https://mirror.invalid/python.pkg.tar.zst",
|
|
264
|
+
),
|
|
265
|
+
clock=lambda: times.pop(0) if len(times) > 1 else times[0],
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
234
269
|
def test_optimizer_limits_active_pool_but_reprobes_inactive_candidates(tmp_path):
|
|
235
270
|
mirrorlist = write_mirrorlist(tmp_path)
|
|
236
271
|
nju = PACMAN_MIRROR_SOURCES[-1][1]
|