railmux 0.4.0.dev30__tar.gz → 0.4.0.dev32__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.dev30/src/railmux.egg-info → railmux-0.4.0.dev32}/PKG-INFO +1 -1
- railmux-0.4.0.dev32/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/cli.py +27 -1
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/app.py +27 -13
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_bootstrap.py +12 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_install_log.py +13 -2
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_msys2.py +339 -12
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_pacman.py +24 -3
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_cli.py +75 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_focus_highlight.py +2 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_sessions_pane.py +33 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_bootstrap.py +30 -1
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_install_log.py +13 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_msys2.py +183 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_pacman.py +35 -0
- railmux-0.4.0.dev30/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/LICENSE +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/README.md +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/pyproject.toml +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/setup.cfg +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_session_lease.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev32"
|
|
@@ -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()
|
|
@@ -2035,21 +2035,35 @@ class App:
|
|
|
2035
2035
|
|
|
2036
2036
|
# --- history preview (display pane shows a transcript, not an agent session) ---
|
|
2037
2037
|
|
|
2038
|
+
def _paint_session_row_click_now(self, session_id: str | None) -> None:
|
|
2039
|
+
"""Acknowledge a pointer selection before synchronous tmux work."""
|
|
2040
|
+
pane = getattr(self, "_sessions_pane", None)
|
|
2041
|
+
if pane is None:
|
|
2042
|
+
return
|
|
2043
|
+
pane.set_selected_session(session_id)
|
|
2044
|
+
loop = getattr(self, "_loop", None)
|
|
2045
|
+
if loop is not None:
|
|
2046
|
+
loop.draw_screen()
|
|
2047
|
+
|
|
2038
2048
|
def _on_session_row_preview(self, session: SessionMeta) -> None:
|
|
2039
2049
|
"""Apply click semantics after rechecking whether the row is live."""
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2050
|
+
self._paint_session_row_click_now(session.session_id)
|
|
2051
|
+
try:
|
|
2052
|
+
running = self._by_session_id(session.session_id)
|
|
2053
|
+
if (running is not None
|
|
2054
|
+
and self._agent_session_alive(running.tmux_name)):
|
|
2055
|
+
self._on_running_select(
|
|
2056
|
+
RunningEntry(
|
|
2057
|
+
tmux_name=running.tmux_name,
|
|
2058
|
+
label=running.label,
|
|
2059
|
+
status=running.status,
|
|
2060
|
+
),
|
|
2061
|
+
steal_focus=False,
|
|
2062
|
+
)
|
|
2063
|
+
return
|
|
2064
|
+
self._on_session_preview(session)
|
|
2065
|
+
finally:
|
|
2066
|
+
self._paint_session_row_click_now(None)
|
|
2053
2067
|
|
|
2054
2068
|
def _on_session_preview(self, session: SessionMeta) -> None:
|
|
2055
2069
|
"""Show the provider's canonical current-branch history.
|
|
@@ -20,6 +20,7 @@ from railmux.windows_msys2 import (
|
|
|
20
20
|
plan_managed_runtime_prune,
|
|
21
21
|
reusable_managed_base_candidate,
|
|
22
22
|
)
|
|
23
|
+
from railmux.windows_paths import managed_windows_data_root
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
_MINIMUM_WINDOWS_PYTHON = (3, 10)
|
|
@@ -49,6 +50,14 @@ def _runtime_status(
|
|
|
49
50
|
) -> int:
|
|
50
51
|
snapshot = managed_runtime_status(
|
|
51
52
|
version=__version__, environ=environ, verify=verify)
|
|
53
|
+
data_root = (
|
|
54
|
+
managed_windows_data_root(environ)
|
|
55
|
+
if runtime is None or runtime.managed
|
|
56
|
+
else None
|
|
57
|
+
)
|
|
58
|
+
if data_root is not None:
|
|
59
|
+
snapshot["data_root"] = str(data_root)
|
|
60
|
+
snapshot["log_directory"] = str(data_root / "logs")
|
|
52
61
|
if json_output:
|
|
53
62
|
json.dump(snapshot, sys.stdout, indent=2, sort_keys=True)
|
|
54
63
|
print()
|
|
@@ -75,6 +84,9 @@ def _runtime_status(
|
|
|
75
84
|
if root is not None:
|
|
76
85
|
print(f"Managed location: {root}")
|
|
77
86
|
print("Next: run 'railmux runtime install' from an interactive PowerShell.")
|
|
87
|
+
if data_root is not None:
|
|
88
|
+
print(f"Data root: {data_root}")
|
|
89
|
+
print(f"Install logs: {data_root / 'logs'}")
|
|
78
90
|
print("Provider data: shared from the Windows user profile")
|
|
79
91
|
content = snapshot.get("content_identity")
|
|
80
92
|
if isinstance(content, str):
|
|
@@ -162,7 +162,12 @@ class InstallReporter:
|
|
|
162
162
|
|
|
163
163
|
def __enter__(self) -> InstallReporter:
|
|
164
164
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
-
|
|
165
|
+
# Windows PowerShell 5 treats BOM-less UTF-8 as the active ANSI code
|
|
166
|
+
# page. The signature keeps Chinese systems from rendering the
|
|
167
|
+
# middle dot/ellipsis as mojibake while remaining ordinary UTF-8 for
|
|
168
|
+
# modern readers (use ``utf-8-sig`` when the exact first character
|
|
169
|
+
# matters).
|
|
170
|
+
self._log = self.path.open("x", encoding="utf-8-sig", newline="\n")
|
|
166
171
|
self._write_log("Railmux Windows runtime installation\n")
|
|
167
172
|
# Windows may defer the visible last-write timestamp of an open file.
|
|
168
173
|
# Never let retention mistake the active evidence log for an old one.
|
|
@@ -324,7 +329,13 @@ class InstallReporter:
|
|
|
324
329
|
if package_count is not None:
|
|
325
330
|
self._package_total = int(package_count.group(1))
|
|
326
331
|
self._package_step = max(1, self._package_total // 10)
|
|
327
|
-
self._progress_detail =
|
|
332
|
+
self._progress_detail = (
|
|
333
|
+
f"waiting for the first transfer among "
|
|
334
|
+
f"{self._package_total} packages"
|
|
335
|
+
)
|
|
336
|
+
self.note(
|
|
337
|
+
f"Package transaction resolved: {self._package_total} packages."
|
|
338
|
+
)
|
|
328
339
|
return
|
|
329
340
|
total_download = _TOTAL_DOWNLOAD_RE.match(line)
|
|
330
341
|
if total_download is not None:
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import concurrent.futures
|
|
10
10
|
import hashlib
|
|
11
11
|
import http.client
|
|
12
|
+
import importlib.metadata as importlib_metadata
|
|
12
13
|
import json
|
|
13
14
|
import lzma
|
|
14
15
|
import os
|
|
@@ -160,6 +161,22 @@ _PACKAGE_COMMAND = (
|
|
|
160
161
|
'--cache-dir "$cache" --only-binary=:all: '
|
|
161
162
|
'--timeout "$4" --retries "$5" "railmux[ssh]==$2"'
|
|
162
163
|
)
|
|
164
|
+
_LOCAL_APP_CHECK_COMMAND = (
|
|
165
|
+
'chmod 755 "$1/venv/bin/railmux" && '
|
|
166
|
+
'"$1/venv/bin/python" -m pip check && '
|
|
167
|
+
'"$1/venv/bin/python" -c '
|
|
168
|
+
"'import packaging, pyte, tomlkit, typing_extensions, urwid, wcwidth, "
|
|
169
|
+
"railmux, sys; assert railmux.__version__ == sys.argv[1]' \"$2\""
|
|
170
|
+
)
|
|
171
|
+
_LOCAL_APP_COPY_FILE_LIMIT = 20_000
|
|
172
|
+
_LOCAL_APP_COPY_SIZE_LIMIT = 128 * 1024 * 1024
|
|
173
|
+
_DEPENDENCY_SEED_NAMES = frozenset(
|
|
174
|
+
{"packaging", "pyte", "tomlkit", "typing_extensions.py", "urwid", "wcwidth"}
|
|
175
|
+
)
|
|
176
|
+
_DEPENDENCY_DIST_INFO_RE = re.compile(
|
|
177
|
+
r"(?:packaging|pyte|tomlkit|typing_extensions|urwid|wcwidth)-.+\.dist-info\Z",
|
|
178
|
+
re.IGNORECASE,
|
|
179
|
+
)
|
|
163
180
|
|
|
164
181
|
|
|
165
182
|
class RuntimeErrorBase(RuntimeError):
|
|
@@ -2454,6 +2471,289 @@ def _validate_managed_application_root(root: Path) -> None:
|
|
|
2454
2471
|
)
|
|
2455
2472
|
|
|
2456
2473
|
|
|
2474
|
+
def _venv_site_packages(application: Path) -> Path | None:
|
|
2475
|
+
"""Return one ordinary MSYS2 venv site-packages directory."""
|
|
2476
|
+
library = application / "venv" / "lib"
|
|
2477
|
+
try:
|
|
2478
|
+
candidates = list(library.glob("python*/site-packages"))
|
|
2479
|
+
except OSError:
|
|
2480
|
+
return None
|
|
2481
|
+
valid: list[Path] = []
|
|
2482
|
+
for candidate in candidates:
|
|
2483
|
+
try:
|
|
2484
|
+
if _path_is_link_or_reparse(candidate) or not candidate.is_dir():
|
|
2485
|
+
continue
|
|
2486
|
+
except OSError:
|
|
2487
|
+
continue
|
|
2488
|
+
valid.append(candidate)
|
|
2489
|
+
return valid[0] if len(valid) == 1 else None
|
|
2490
|
+
|
|
2491
|
+
|
|
2492
|
+
def _native_application_payload(version: str) -> tuple[Path, Path] | None:
|
|
2493
|
+
"""Locate the currently executing pure-Python package and dist-info."""
|
|
2494
|
+
try:
|
|
2495
|
+
distribution = importlib_metadata.distribution("railmux")
|
|
2496
|
+
except importlib_metadata.PackageNotFoundError:
|
|
2497
|
+
return None
|
|
2498
|
+
if distribution.version != version:
|
|
2499
|
+
return None
|
|
2500
|
+
package = Path(__file__).resolve().parent
|
|
2501
|
+
if not package.is_dir():
|
|
2502
|
+
return None
|
|
2503
|
+
info_names = {
|
|
2504
|
+
Path(str(item)).parts[0]
|
|
2505
|
+
for item in distribution.files or ()
|
|
2506
|
+
if Path(str(item)).parts
|
|
2507
|
+
and Path(str(item)).parts[0].casefold().startswith("railmux-")
|
|
2508
|
+
and Path(str(item)).parts[0].casefold().endswith(".dist-info")
|
|
2509
|
+
}
|
|
2510
|
+
if len(info_names) != 1:
|
|
2511
|
+
return None
|
|
2512
|
+
info = Path(distribution.locate_file(next(iter(info_names))))
|
|
2513
|
+
if not info.is_dir():
|
|
2514
|
+
return None
|
|
2515
|
+
return package, info
|
|
2516
|
+
|
|
2517
|
+
|
|
2518
|
+
def _copy_local_tree(
|
|
2519
|
+
source: Path,
|
|
2520
|
+
destination: Path,
|
|
2521
|
+
*,
|
|
2522
|
+
budget: list[int],
|
|
2523
|
+
ignored_names: frozenset[str] = frozenset(),
|
|
2524
|
+
) -> None:
|
|
2525
|
+
"""Copy one bounded tree without following links or reparse points."""
|
|
2526
|
+
try:
|
|
2527
|
+
if _path_is_link_or_reparse(source) or not source.is_dir():
|
|
2528
|
+
raise RuntimeInstallError("a local application seed tree was unsafe")
|
|
2529
|
+
except OSError as exc:
|
|
2530
|
+
raise RuntimeInstallError(
|
|
2531
|
+
"a local application seed tree could not be inspected"
|
|
2532
|
+
) from exc
|
|
2533
|
+
destination.mkdir()
|
|
2534
|
+
for current_text, directories, files in os.walk(source, followlinks=False):
|
|
2535
|
+
current = Path(current_text)
|
|
2536
|
+
relative = current.relative_to(source)
|
|
2537
|
+
target_root = destination / relative
|
|
2538
|
+
kept_directories: list[str] = []
|
|
2539
|
+
for name in directories:
|
|
2540
|
+
if name in ignored_names or name == "__pycache__":
|
|
2541
|
+
continue
|
|
2542
|
+
child = current / name
|
|
2543
|
+
try:
|
|
2544
|
+
if _path_is_link_or_reparse(child) or not child.is_dir():
|
|
2545
|
+
raise RuntimeInstallError(
|
|
2546
|
+
"a local application seed contained an unsafe directory"
|
|
2547
|
+
)
|
|
2548
|
+
except OSError as exc:
|
|
2549
|
+
raise RuntimeInstallError(
|
|
2550
|
+
"a local application seed could not be inspected"
|
|
2551
|
+
) from exc
|
|
2552
|
+
kept_directories.append(name)
|
|
2553
|
+
(target_root / name).mkdir()
|
|
2554
|
+
directories[:] = kept_directories
|
|
2555
|
+
for name in files:
|
|
2556
|
+
if (
|
|
2557
|
+
name in ignored_names
|
|
2558
|
+
or name.endswith((".pyc", ".pyo"))
|
|
2559
|
+
or name.endswith((".pth", ".egg-link"))
|
|
2560
|
+
):
|
|
2561
|
+
continue
|
|
2562
|
+
child = current / name
|
|
2563
|
+
try:
|
|
2564
|
+
info = child.lstat()
|
|
2565
|
+
if (
|
|
2566
|
+
_path_is_link_or_reparse(child)
|
|
2567
|
+
or not stat.S_ISREG(info.st_mode)
|
|
2568
|
+
):
|
|
2569
|
+
raise RuntimeInstallError(
|
|
2570
|
+
"a local application seed contained an unsafe file"
|
|
2571
|
+
)
|
|
2572
|
+
except OSError as exc:
|
|
2573
|
+
raise RuntimeInstallError(
|
|
2574
|
+
"a local application seed could not be inspected"
|
|
2575
|
+
) from exc
|
|
2576
|
+
budget[0] += 1
|
|
2577
|
+
budget[1] += info.st_size
|
|
2578
|
+
if (
|
|
2579
|
+
budget[0] > _LOCAL_APP_COPY_FILE_LIMIT
|
|
2580
|
+
or budget[1] > _LOCAL_APP_COPY_SIZE_LIMIT
|
|
2581
|
+
):
|
|
2582
|
+
raise RuntimeInstallError(
|
|
2583
|
+
"the local application seed exceeded its safety bound"
|
|
2584
|
+
)
|
|
2585
|
+
# Do not carry an NTFS read-only attribute from a packaged-Python
|
|
2586
|
+
# cache or an older app layer into the new writable venv.
|
|
2587
|
+
shutil.copyfile(child, target_root / name)
|
|
2588
|
+
|
|
2589
|
+
|
|
2590
|
+
def _copy_local_file(source: Path, destination: Path, *, budget: list[int]) -> None:
|
|
2591
|
+
try:
|
|
2592
|
+
info = source.lstat()
|
|
2593
|
+
if _path_is_link_or_reparse(source) or not stat.S_ISREG(info.st_mode):
|
|
2594
|
+
raise RuntimeInstallError("a local dependency seed file was unsafe")
|
|
2595
|
+
except OSError as exc:
|
|
2596
|
+
raise RuntimeInstallError(
|
|
2597
|
+
"a local dependency seed file could not be inspected"
|
|
2598
|
+
) from exc
|
|
2599
|
+
budget[0] += 1
|
|
2600
|
+
budget[1] += info.st_size
|
|
2601
|
+
if (
|
|
2602
|
+
budget[0] > _LOCAL_APP_COPY_FILE_LIMIT
|
|
2603
|
+
or budget[1] > _LOCAL_APP_COPY_SIZE_LIMIT
|
|
2604
|
+
):
|
|
2605
|
+
raise RuntimeInstallError("the local application seed exceeded its safety bound")
|
|
2606
|
+
shutil.copyfile(source, destination)
|
|
2607
|
+
|
|
2608
|
+
|
|
2609
|
+
def _dependency_seed_entries(site_packages: Path) -> list[Path] | None:
|
|
2610
|
+
try:
|
|
2611
|
+
entries = list(site_packages.iterdir())
|
|
2612
|
+
except OSError:
|
|
2613
|
+
return None
|
|
2614
|
+
selected = [
|
|
2615
|
+
entry
|
|
2616
|
+
for entry in entries
|
|
2617
|
+
if entry.name in _DEPENDENCY_SEED_NAMES
|
|
2618
|
+
or _DEPENDENCY_DIST_INFO_RE.fullmatch(entry.name)
|
|
2619
|
+
]
|
|
2620
|
+
names = {entry.name for entry in selected}
|
|
2621
|
+
if not _DEPENDENCY_SEED_NAMES.issubset(names):
|
|
2622
|
+
return None
|
|
2623
|
+
for dependency in _DEPENDENCY_SEED_NAMES - {"typing_extensions.py"}:
|
|
2624
|
+
if not any(
|
|
2625
|
+
name.casefold().startswith(f"{dependency.casefold()}-")
|
|
2626
|
+
and name.casefold().endswith(".dist-info")
|
|
2627
|
+
for name in names
|
|
2628
|
+
):
|
|
2629
|
+
return None
|
|
2630
|
+
if not any(
|
|
2631
|
+
name.casefold().startswith("typing_extensions-")
|
|
2632
|
+
and name.casefold().endswith(".dist-info")
|
|
2633
|
+
for name in names
|
|
2634
|
+
):
|
|
2635
|
+
return None
|
|
2636
|
+
return selected
|
|
2637
|
+
|
|
2638
|
+
|
|
2639
|
+
def _verified_dependency_seed(
|
|
2640
|
+
root: Path,
|
|
2641
|
+
*,
|
|
2642
|
+
version: str,
|
|
2643
|
+
environ: Mapping[str, str],
|
|
2644
|
+
probe: Probe,
|
|
2645
|
+
) -> tuple[str, Path] | None:
|
|
2646
|
+
marked = _marked_app_versions(root)
|
|
2647
|
+
if not marked:
|
|
2648
|
+
return None
|
|
2649
|
+
for candidate_version in sorted(marked, key=_version_key, reverse=True):
|
|
2650
|
+
if candidate_version == version:
|
|
2651
|
+
continue
|
|
2652
|
+
app_name = _app_name(candidate_version)
|
|
2653
|
+
candidate = Msys2Runtime(root, managed=False, app_name=app_name)
|
|
2654
|
+
if not probe_runtime(
|
|
2655
|
+
candidate,
|
|
2656
|
+
version=candidate_version,
|
|
2657
|
+
environ=environ,
|
|
2658
|
+
probe=probe,
|
|
2659
|
+
):
|
|
2660
|
+
continue
|
|
2661
|
+
site_packages = _venv_site_packages(marked[candidate_version])
|
|
2662
|
+
if (
|
|
2663
|
+
site_packages is not None
|
|
2664
|
+
and _dependency_seed_entries(site_packages) is not None
|
|
2665
|
+
):
|
|
2666
|
+
return candidate_version, site_packages
|
|
2667
|
+
return None
|
|
2668
|
+
|
|
2669
|
+
|
|
2670
|
+
def _install_application_from_local_seed(
|
|
2671
|
+
root: Path,
|
|
2672
|
+
*,
|
|
2673
|
+
final_app: Path,
|
|
2674
|
+
final_posix: str,
|
|
2675
|
+
version: str,
|
|
2676
|
+
environ: Mapping[str, str],
|
|
2677
|
+
reporter: InstallReporter,
|
|
2678
|
+
runner: Runner | None,
|
|
2679
|
+
probe: Probe,
|
|
2680
|
+
) -> str:
|
|
2681
|
+
"""Build an upgrade layer locally; return ready, unavailable, or failed."""
|
|
2682
|
+
payload = _native_application_payload(version)
|
|
2683
|
+
seed = _verified_dependency_seed(
|
|
2684
|
+
root, version=version, environ=environ, probe=probe)
|
|
2685
|
+
target_site = _venv_site_packages(final_app)
|
|
2686
|
+
if payload is None or seed is None or target_site is None:
|
|
2687
|
+
return "unavailable"
|
|
2688
|
+
source_package, source_info = payload
|
|
2689
|
+
seed_version, seed_site = seed
|
|
2690
|
+
seed_entries = _dependency_seed_entries(seed_site)
|
|
2691
|
+
if seed_entries is None:
|
|
2692
|
+
return "unavailable"
|
|
2693
|
+
budget = [0, 0]
|
|
2694
|
+
try:
|
|
2695
|
+
for source in seed_entries:
|
|
2696
|
+
destination = target_site / source.name
|
|
2697
|
+
if destination.exists() or destination.is_symlink():
|
|
2698
|
+
raise RuntimeInstallError(
|
|
2699
|
+
"the new application environment unexpectedly contained "
|
|
2700
|
+
f"{source.name}"
|
|
2701
|
+
)
|
|
2702
|
+
if source.is_dir():
|
|
2703
|
+
_copy_local_tree(source, destination, budget=budget)
|
|
2704
|
+
else:
|
|
2705
|
+
_copy_local_file(source, destination, budget=budget)
|
|
2706
|
+
_copy_local_tree(
|
|
2707
|
+
source_package,
|
|
2708
|
+
target_site / "railmux",
|
|
2709
|
+
budget=budget,
|
|
2710
|
+
)
|
|
2711
|
+
_copy_local_tree(
|
|
2712
|
+
source_info,
|
|
2713
|
+
target_site / source_info.name,
|
|
2714
|
+
budget=budget,
|
|
2715
|
+
ignored_names=frozenset(
|
|
2716
|
+
{"RECORD", "direct_url.json", "INSTALLER", "REQUESTED"}
|
|
2717
|
+
),
|
|
2718
|
+
)
|
|
2719
|
+
entrypoint = final_app / "venv" / "bin" / "railmux"
|
|
2720
|
+
with entrypoint.open("x", encoding="utf-8", newline="\n") as stream:
|
|
2721
|
+
stream.write(
|
|
2722
|
+
f"#!{final_posix}/venv/bin/python\n"
|
|
2723
|
+
"from railmux.entrypoint import main\n"
|
|
2724
|
+
"raise SystemExit(main())\n"
|
|
2725
|
+
)
|
|
2726
|
+
except (OSError, shutil.Error, RuntimeInstallError) as exc:
|
|
2727
|
+
reporter.note(
|
|
2728
|
+
"Verified local app-layer reuse was unavailable "
|
|
2729
|
+
f"({exc}); falling back to the private PyPI cache.",
|
|
2730
|
+
level="warning",
|
|
2731
|
+
)
|
|
2732
|
+
return "failed"
|
|
2733
|
+
returncode = _run_checked(
|
|
2734
|
+
_bash_command(root, _LOCAL_APP_CHECK_COMMAND, final_posix, version),
|
|
2735
|
+
env=Msys2Runtime(root, managed=False).environment(environ),
|
|
2736
|
+
reporter=reporter,
|
|
2737
|
+
runner=runner,
|
|
2738
|
+
label="Local Railmux application validation",
|
|
2739
|
+
allow_failure=True,
|
|
2740
|
+
)
|
|
2741
|
+
if returncode:
|
|
2742
|
+
reporter.note(
|
|
2743
|
+
"The locally reused dependency layer did not satisfy the current "
|
|
2744
|
+
"package contract; falling back to the private PyPI cache.",
|
|
2745
|
+
level="warning",
|
|
2746
|
+
)
|
|
2747
|
+
return "failed"
|
|
2748
|
+
reporter.note(
|
|
2749
|
+
f"Reused verified dependencies from Railmux {seed_version}; installed "
|
|
2750
|
+
f"Railmux {version} from the current Windows package without network "
|
|
2751
|
+
"access.",
|
|
2752
|
+
level="success",
|
|
2753
|
+
)
|
|
2754
|
+
return "ready"
|
|
2755
|
+
|
|
2756
|
+
|
|
2457
2757
|
def _install_application(
|
|
2458
2758
|
root: Path,
|
|
2459
2759
|
*,
|
|
@@ -2534,21 +2834,48 @@ def _install_application(
|
|
|
2534
2834
|
runner=runner,
|
|
2535
2835
|
label="Railmux virtual environment creation",
|
|
2536
2836
|
)
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
)
|
|
2544
|
-
package_returncode = _run_checked(
|
|
2545
|
-
_bash_command(root, _PACKAGE_COMMAND, *package_arguments),
|
|
2546
|
-
env=child_env,
|
|
2837
|
+
local_install = _install_application_from_local_seed(
|
|
2838
|
+
root,
|
|
2839
|
+
final_app=final_app,
|
|
2840
|
+
final_posix=final_posix,
|
|
2841
|
+
version=version,
|
|
2842
|
+
environ=environ,
|
|
2547
2843
|
reporter=reporter,
|
|
2548
2844
|
runner=runner,
|
|
2549
|
-
|
|
2550
|
-
allow_failure=True,
|
|
2845
|
+
probe=probe,
|
|
2551
2846
|
)
|
|
2847
|
+
if local_install == "failed":
|
|
2848
|
+
try:
|
|
2849
|
+
shutil.rmtree(final_app / "venv")
|
|
2850
|
+
except OSError as exc:
|
|
2851
|
+
raise RuntimeInstallError(
|
|
2852
|
+
"could not reset the unpublished local application seed"
|
|
2853
|
+
) from exc
|
|
2854
|
+
_run_checked(
|
|
2855
|
+
_bash_command(root, _VENV_COMMAND, final_posix),
|
|
2856
|
+
env=child_env,
|
|
2857
|
+
reporter=reporter,
|
|
2858
|
+
runner=runner,
|
|
2859
|
+
label="Railmux virtual environment recovery",
|
|
2860
|
+
)
|
|
2861
|
+
if local_install == "ready":
|
|
2862
|
+
package_returncode = 0
|
|
2863
|
+
else:
|
|
2864
|
+
package_arguments = (
|
|
2865
|
+
final_posix,
|
|
2866
|
+
version,
|
|
2867
|
+
str(cache),
|
|
2868
|
+
str(_PIP_INITIAL_TIMEOUT_SECONDS),
|
|
2869
|
+
str(_PIP_INITIAL_RETRIES),
|
|
2870
|
+
)
|
|
2871
|
+
package_returncode = _run_checked(
|
|
2872
|
+
_bash_command(root, _PACKAGE_COMMAND, *package_arguments),
|
|
2873
|
+
env=child_env,
|
|
2874
|
+
reporter=reporter,
|
|
2875
|
+
runner=runner,
|
|
2876
|
+
label="Railmux runtime package installation",
|
|
2877
|
+
allow_failure=True,
|
|
2878
|
+
)
|
|
2552
2879
|
if package_returncode:
|
|
2553
2880
|
network_failure = (
|
|
2554
2881
|
reporter.command_had_network_failure
|
|
@@ -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(
|