railmux 0.4.0.dev9__tar.gz → 0.4.0.dev11__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.dev9/src/railmux.egg-info → railmux-0.4.0.dev11}/PKG-INFO +1 -1
- railmux-0.4.0.dev11/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/cli.py +16 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/codex_index.py +2 -2
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/discovery.py +2 -1
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_server.py +1 -1
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/mouse_manager.py +1 -1
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/orphan_marker.py +2 -2
- railmux-0.4.0.dev11/src/railmux/provider_paths.py +102 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/restart_state.py +8 -2
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tmux_binding_manager.py +1 -1
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/app.py +21 -1
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/windows_bootstrap.py +28 -7
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/windows_msys2.py +399 -53
- railmux-0.4.0.dev11/src/railmux/windows_tmux_lifecycle.py +259 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/SOURCES.txt +2 -0
- railmux-0.4.0.dev11/tests/test_provider_paths.py +110 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_restart_state.py +30 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_soft_quit.py +6 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_bootstrap.py +77 -1
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_msys2.py +318 -23
- railmux-0.4.0.dev11/tests/test_windows_tmux_lifecycle.py +140 -0
- railmux-0.4.0.dev9/src/railmux/__init__.py +0 -1
- railmux-0.4.0.dev9/src/railmux/provider_paths.py +0 -41
- railmux-0.4.0.dev9/tests/test_provider_paths.py +0 -29
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/LICENSE +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/README.md +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/pyproject.toml +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/setup.cfg +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_cli.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev11"
|
|
@@ -20,6 +20,7 @@ from railmux.runtime_config import (
|
|
|
20
20
|
)
|
|
21
21
|
from railmux import tmux_health
|
|
22
22
|
from railmux import tmux_server
|
|
23
|
+
from railmux import windows_tmux_lifecycle
|
|
23
24
|
from railmux.system_deps import ensure_tmux_available
|
|
24
25
|
from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
|
|
25
26
|
|
|
@@ -126,7 +127,16 @@ def _run_tmux_client_with_watchdog(
|
|
|
126
127
|
file=sys.stderr,
|
|
127
128
|
)
|
|
128
129
|
return 2
|
|
130
|
+
# tmux may leave the client TTY in raw mode even after its process
|
|
131
|
+
# exits. Restore it before a bounded recovery probe or user message.
|
|
132
|
+
_restore_terminal(attributes)
|
|
129
133
|
returncode = process.returncode or 0
|
|
134
|
+
if windows_tmux_lifecycle.recover_abandoned_socket():
|
|
135
|
+
print(
|
|
136
|
+
"info: cleaned an abandoned Windows tmux socket; Codex and "
|
|
137
|
+
"Claude session files were not changed",
|
|
138
|
+
file=sys.stderr,
|
|
139
|
+
)
|
|
130
140
|
if returncode and expected_target is not None:
|
|
131
141
|
try:
|
|
132
142
|
current_target = tmux_server.discover_target(timeout=1.0)
|
|
@@ -367,6 +377,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
367
377
|
# Validate before any command can address a tmux server. ``tmux -V``
|
|
368
378
|
# above is server-independent.
|
|
369
379
|
tmux_server.socket_label()
|
|
380
|
+
if windows_tmux_lifecycle.recover_abandoned_socket():
|
|
381
|
+
print(
|
|
382
|
+
"info: cleaned an abandoned Windows tmux socket; Codex and "
|
|
383
|
+
"Claude session files were not changed",
|
|
384
|
+
file=sys.stderr,
|
|
385
|
+
)
|
|
370
386
|
dedicated_target = tmux_server.discover_target()
|
|
371
387
|
on_dedicated_server = tmux_server.is_current_server(dedicated_target)
|
|
372
388
|
except tmux_server.TmuxServerError as exc:
|
|
@@ -25,7 +25,7 @@ from railmux.models import (
|
|
|
25
25
|
Project,
|
|
26
26
|
SessionMeta,
|
|
27
27
|
)
|
|
28
|
-
from railmux.provider_paths import provider_path
|
|
28
|
+
from railmux.provider_paths import private_mode_is_safe, provider_path
|
|
29
29
|
from railmux.renames import Renames
|
|
30
30
|
|
|
31
31
|
# Codex has no reliable provider-neutral signal that distinguishes a long
|
|
@@ -461,7 +461,7 @@ class CodexIndex:
|
|
|
461
461
|
if (
|
|
462
462
|
not stat.S_ISREG(info.st_mode)
|
|
463
463
|
or info.st_uid != os.getuid()
|
|
464
|
-
or info.st_mode
|
|
464
|
+
or not private_mode_is_safe(info.st_mode)
|
|
465
465
|
or info.st_size > _PERSISTENT_CACHE_MAX_BYTES
|
|
466
466
|
):
|
|
467
467
|
return
|
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from railmux.atomic_file import atomic_write_text
|
|
11
11
|
from railmux.models import Project
|
|
12
12
|
from railmux.path_codec import decode, is_encoded_project_name
|
|
13
|
+
from railmux.provider_paths import private_mode_is_safe
|
|
13
14
|
from railmux.session_index import _looks_like_uuid, _scan_session
|
|
14
15
|
|
|
15
16
|
|
|
@@ -55,7 +56,7 @@ def _load_persistent_validity(claude_home: Path) -> None:
|
|
|
55
56
|
not stat.S_ISREG(info.st_mode)
|
|
56
57
|
or info.st_uid != os.getuid()
|
|
57
58
|
or info.st_size > _VALIDITY_CACHE_LIMIT
|
|
58
|
-
or info.st_mode
|
|
59
|
+
or not private_mode_is_safe(info.st_mode)
|
|
59
60
|
):
|
|
60
61
|
return
|
|
61
62
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -1493,7 +1493,7 @@ def _acquire_display_lock(
|
|
|
1493
1493
|
if (
|
|
1494
1494
|
not stat.S_ISREG(info.st_mode)
|
|
1495
1495
|
or info.st_uid != os.getuid()
|
|
1496
|
-
or info.st_mode
|
|
1496
|
+
or not restart_state.private_mode_is_safe(info.st_mode)
|
|
1497
1497
|
):
|
|
1498
1498
|
raise OSError("unsafe display lock")
|
|
1499
1499
|
deadline = time.monotonic() + max(0.0, timeout)
|
|
@@ -60,7 +60,7 @@ class RootWheelForwardingManager:
|
|
|
60
60
|
info = self._state_path.lstat()
|
|
61
61
|
if (not stat.S_ISREG(info.st_mode)
|
|
62
62
|
or info.st_uid != os.getuid()
|
|
63
|
-
or info.st_mode
|
|
63
|
+
or not restart_state.private_mode_is_safe(info.st_mode)
|
|
64
64
|
or info.st_size > _MAX_STATE_BYTES):
|
|
65
65
|
return None
|
|
66
66
|
raw = json.loads(self._state_path.read_text(encoding="utf-8"))
|
|
@@ -231,7 +231,7 @@ def claim_owner(
|
|
|
231
231
|
parent = lock_path.parent.stat()
|
|
232
232
|
if (not stat.S_ISDIR(parent.st_mode)
|
|
233
233
|
or parent.st_uid != os.getuid()
|
|
234
|
-
or parent.st_mode
|
|
234
|
+
or not restart_state.private_mode_is_safe(parent.st_mode)):
|
|
235
235
|
return None
|
|
236
236
|
flags = os.O_CREAT | os.O_RDWR
|
|
237
237
|
if hasattr(os, "O_NOFOLLOW"):
|
|
@@ -243,7 +243,7 @@ def claim_owner(
|
|
|
243
243
|
info = os.fstat(fd)
|
|
244
244
|
if (not stat.S_ISREG(info.st_mode)
|
|
245
245
|
or info.st_uid != os.getuid()
|
|
246
|
-
or info.st_mode
|
|
246
|
+
or not restart_state.private_mode_is_safe(info.st_mode)):
|
|
247
247
|
return None
|
|
248
248
|
try:
|
|
249
249
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Translate provider-owned native Windows paths inside the MSYS2 preview."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import stat
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_DRIVE_PATH = re.compile(r"^(?:\\\\\?\\)?([A-Za-z]):[\\/](.*)\Z")
|
|
14
|
+
_MANAGED_BASE_MARKER = Path("/railmux-base.json")
|
|
15
|
+
_MANAGED_APP_ROOT = Path("/opt/railmux/apps")
|
|
16
|
+
_MANAGED_APP_ID = re.compile(
|
|
17
|
+
r"railmux-[0-9]+(?:\.[0-9]+)*(?:\.dev[0-9]+)?\Z"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def running_in_windows_wrapper(environ: Mapping[str, str] = os.environ) -> bool:
|
|
22
|
+
"""Return whether Railmux is running in its native-Windows MSYS2 wrapper."""
|
|
23
|
+
return environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def running_in_managed_windows_wrapper(
|
|
27
|
+
environ: Mapping[str, str] = os.environ,
|
|
28
|
+
) -> bool:
|
|
29
|
+
"""Verify the installed MSYS2 runtime, not merely its environment hint."""
|
|
30
|
+
if sys.platform != "cygwin" or not running_in_windows_wrapper(environ):
|
|
31
|
+
return False
|
|
32
|
+
runtime_id = environ.get("RAILMUX_MSYS2_RUNTIME_ID")
|
|
33
|
+
app_id = environ.get("RAILMUX_MSYS2_APP_ID")
|
|
34
|
+
if (
|
|
35
|
+
not isinstance(runtime_id, str)
|
|
36
|
+
or not runtime_id
|
|
37
|
+
or not isinstance(app_id, str)
|
|
38
|
+
or _MANAGED_APP_ID.fullmatch(app_id) is None
|
|
39
|
+
):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
def read_safe_marker(marker: Path) -> object | None:
|
|
43
|
+
try:
|
|
44
|
+
info = marker.lstat()
|
|
45
|
+
if (
|
|
46
|
+
not stat.S_ISREG(info.st_mode)
|
|
47
|
+
or info.st_uid != os.getuid()
|
|
48
|
+
or info.st_mode & 0o022
|
|
49
|
+
or info.st_size > 4096
|
|
50
|
+
):
|
|
51
|
+
return None
|
|
52
|
+
return json.loads(marker.read_text(encoding="utf-8"))
|
|
53
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
base_payload = read_safe_marker(_MANAGED_BASE_MARKER)
|
|
57
|
+
app_payload = read_safe_marker(
|
|
58
|
+
_MANAGED_APP_ROOT / app_id / "railmux-app.json"
|
|
59
|
+
)
|
|
60
|
+
from railmux import __version__
|
|
61
|
+
|
|
62
|
+
return base_payload == {
|
|
63
|
+
"schema": 1,
|
|
64
|
+
"runtime": runtime_id,
|
|
65
|
+
} and app_payload == {
|
|
66
|
+
"schema": 1,
|
|
67
|
+
"runtime": runtime_id,
|
|
68
|
+
"railmux": __version__,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def private_mode_is_safe(mode: int) -> bool:
|
|
73
|
+
"""Accept strict POSIX modes or the verified MSYS2 noacl projection."""
|
|
74
|
+
return not mode & 0o077 or (
|
|
75
|
+
running_in_managed_windows_wrapper() and not mode & 0o022
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def provider_path(
|
|
80
|
+
raw: str,
|
|
81
|
+
*,
|
|
82
|
+
environ: Mapping[str, str] = os.environ,
|
|
83
|
+
) -> Path:
|
|
84
|
+
"""Return a POSIX-visible path for provider metadata.
|
|
85
|
+
|
|
86
|
+
Native providers write Windows paths into session metadata. Railmux runs
|
|
87
|
+
under MSYS2, where the same locations are mounted as ``/c/...`` (or
|
|
88
|
+
``//server/share/...`` for UNC paths). Outside the wrapper, preserve the
|
|
89
|
+
existing POSIX behavior exactly.
|
|
90
|
+
"""
|
|
91
|
+
if not running_in_windows_wrapper(environ):
|
|
92
|
+
return Path(raw)
|
|
93
|
+
|
|
94
|
+
match = _DRIVE_PATH.match(raw)
|
|
95
|
+
if match is not None:
|
|
96
|
+
drive, tail = match.groups()
|
|
97
|
+
return Path(f"/{drive.lower()}/{tail.replace(chr(92), '/')}")
|
|
98
|
+
|
|
99
|
+
if raw.startswith("\\\\"):
|
|
100
|
+
return Path("//" + raw[2:].replace("\\", "/"))
|
|
101
|
+
|
|
102
|
+
return Path(raw.replace("\\", "/"))
|
|
@@ -17,6 +17,7 @@ from pathlib import Path
|
|
|
17
17
|
from railmux import tmux_ctl
|
|
18
18
|
from railmux.atomic_file import atomic_write_text
|
|
19
19
|
from railmux.config import default_config_path
|
|
20
|
+
from railmux.provider_paths import private_mode_is_safe
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
SCHEMA_VERSION = 1
|
|
@@ -164,10 +165,15 @@ def legacy_state_path() -> Path:
|
|
|
164
165
|
def _ensure_private_dir(path: Path) -> None:
|
|
165
166
|
"""Create and verify a Railmux-owned 0700 runtime directory."""
|
|
166
167
|
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
167
|
-
info = path.
|
|
168
|
+
info = path.lstat()
|
|
169
|
+
# MSYS2's default noacl projection reports user-owned NTFS directories as
|
|
170
|
+
# 0755 even after chmod(0700). The managed runtime itself lives below the
|
|
171
|
+
# Windows user's LocalAppData ACL. Accept that projection only on the real
|
|
172
|
+
# Cygwin/MSYS Python platform, and still reject symlinks, foreign owners,
|
|
173
|
+
# and any group/world-writable directory. POSIX platforms retain 0700.
|
|
168
174
|
if (not stat.S_ISDIR(info.st_mode)
|
|
169
175
|
or info.st_uid != os.getuid()
|
|
170
|
-
or info.st_mode
|
|
176
|
+
or not private_mode_is_safe(info.st_mode)):
|
|
171
177
|
raise OSError("Railmux runtime state directory is not private")
|
|
172
178
|
|
|
173
179
|
|
|
@@ -85,7 +85,7 @@ class SharedTmuxBindingManager:
|
|
|
85
85
|
info = self._state_path.lstat()
|
|
86
86
|
if (not stat.S_ISREG(info.st_mode)
|
|
87
87
|
or info.st_uid != os.getuid()
|
|
88
|
-
or info.st_mode
|
|
88
|
+
or not restart_state.private_mode_is_safe(info.st_mode)
|
|
89
89
|
or info.st_size > _MAX_STATE_BYTES):
|
|
90
90
|
return None
|
|
91
91
|
raw = json.loads(self._state_path.read_text(encoding="utf-8"))
|
|
@@ -25,6 +25,7 @@ from railmux import (
|
|
|
25
25
|
tmux_ctl,
|
|
26
26
|
tmux_health,
|
|
27
27
|
tmux_server,
|
|
28
|
+
windows_tmux_lifecycle,
|
|
28
29
|
)
|
|
29
30
|
from railmux.atomic_file import atomic_write_text
|
|
30
31
|
from railmux.background_index import BackgroundCodexIndex
|
|
@@ -713,6 +714,9 @@ class App:
|
|
|
713
714
|
# digest plus immutable pane id namespaces local recovery state across
|
|
714
715
|
# windows, sessions, and private tmux servers.
|
|
715
716
|
self._restart_identity = restart_state.capture_outer_identity()
|
|
717
|
+
# A newly live UI revokes any cleanup proof from the preceding
|
|
718
|
+
# lifecycle before it can create provider sessions.
|
|
719
|
+
windows_tmux_lifecycle.clear_empty_server_exit()
|
|
716
720
|
self._claude_home = claude_home
|
|
717
721
|
self._config = config
|
|
718
722
|
self._auto_launched = auto_launched
|
|
@@ -8061,10 +8065,25 @@ class App:
|
|
|
8061
8065
|
if defer_outer or getattr(self, "_outer_teardown_done", False):
|
|
8062
8066
|
return
|
|
8063
8067
|
self._outer_teardown_done = True
|
|
8068
|
+
identity = getattr(self, "_restart_identity", None)
|
|
8069
|
+
if (
|
|
8070
|
+
self._auto_launched
|
|
8071
|
+
and identity is not None
|
|
8072
|
+
and running_in_windows_wrapper()
|
|
8073
|
+
and tmux_ctl.current_session_name() == "railmux"
|
|
8074
|
+
and tmux_ctl.current_session_id() == identity.session_id
|
|
8075
|
+
):
|
|
8076
|
+
# MSYS2 can retain an AF_UNIX pathname after its last session
|
|
8077
|
+
# exits. Arm cleanup only when the server-wide immutable-ID
|
|
8078
|
+
# inventory proves that no provider or unknown session exists.
|
|
8079
|
+
windows_tmux_lifecycle.arm_empty_server_exit(
|
|
8080
|
+
server_pid=identity.server_pid,
|
|
8081
|
+
session_id=identity.session_id,
|
|
8082
|
+
pane_id=identity.pane_id,
|
|
8083
|
+
)
|
|
8064
8084
|
if not self._soft_quit_flag and self._auto_launched:
|
|
8065
8085
|
session_name = tmux_ctl.current_session_name()
|
|
8066
8086
|
if session_name == "railmux":
|
|
8067
|
-
identity = getattr(self, "_restart_identity", None)
|
|
8068
8087
|
current_id = tmux_ctl.current_session_id()
|
|
8069
8088
|
intent = bool(
|
|
8070
8089
|
identity is not None
|
|
@@ -8080,6 +8099,7 @@ class App:
|
|
|
8080
8099
|
killed = False
|
|
8081
8100
|
if intent and not killed:
|
|
8082
8101
|
tmux_health.clear_clean_exit()
|
|
8102
|
+
windows_tmux_lifecycle.clear_empty_server_exit()
|
|
8083
8103
|
|
|
8084
8104
|
def _enter_filter_mode(self) -> None:
|
|
8085
8105
|
# Borrow the button row (footer index 1) for a filter Edit — both are a
|
|
@@ -14,6 +14,7 @@ from railmux.windows_msys2 import (
|
|
|
14
14
|
find_runtime,
|
|
15
15
|
install_managed_runtime,
|
|
16
16
|
managed_root,
|
|
17
|
+
reusable_managed_base_candidate,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
@@ -45,7 +46,7 @@ def _runtime_status(
|
|
|
45
46
|
ownership = "Railmux-managed" if runtime.managed else "user-owned override"
|
|
46
47
|
print(f"Runtime: {ownership} at {runtime.root}")
|
|
47
48
|
else:
|
|
48
|
-
root = managed_root(environ
|
|
49
|
+
root = managed_root(environ)
|
|
49
50
|
if root is not None:
|
|
50
51
|
print(f"Managed location: {root}")
|
|
51
52
|
print("Provider data: shared from the Windows user profile")
|
|
@@ -55,7 +56,8 @@ def _runtime_status(
|
|
|
55
56
|
def _confirm_install(*, input_fn: Callable[[str], str] = input) -> bool:
|
|
56
57
|
try:
|
|
57
58
|
answer = input_fn(
|
|
58
|
-
"Install the private MSYS2/tmux
|
|
59
|
+
"No reusable MSYS2 base was found. Install the private MSYS2/tmux "
|
|
60
|
+
"runtime now? On Windows, Railmux "
|
|
59
61
|
"depends on a complete private MSYS2 compatibility wrapper, "
|
|
60
62
|
"including tmux and Python. This downloads a 50 MB base plus "
|
|
61
63
|
"required updates and packages, "
|
|
@@ -74,14 +76,30 @@ def _install(
|
|
|
74
76
|
verbose: bool = False,
|
|
75
77
|
input_fn: Callable[[str], str] = input,
|
|
76
78
|
) -> Msys2Runtime | None:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
reuse_only = False
|
|
80
|
+
if not assume_yes:
|
|
81
|
+
reusable = reusable_managed_base_candidate(environ)
|
|
82
|
+
if reusable is None:
|
|
83
|
+
if not _confirm_install(input_fn=input_fn):
|
|
84
|
+
print("MSYS2 runtime installation cancelled.", file=sys.stderr)
|
|
85
|
+
return None
|
|
86
|
+
else:
|
|
87
|
+
_root, source_version = reusable
|
|
88
|
+
detail = (
|
|
89
|
+
f" from Railmux {source_version}" if source_version else ""
|
|
90
|
+
)
|
|
91
|
+
print(
|
|
92
|
+
f"Reusing the matching MSYS2 {MSYS2_RELEASE} private base"
|
|
93
|
+
f"{detail}; only the Railmux {__version__} app layer will be "
|
|
94
|
+
"installed."
|
|
95
|
+
)
|
|
96
|
+
reuse_only = True
|
|
80
97
|
try:
|
|
81
98
|
runtime = install_managed_runtime(
|
|
82
99
|
version=__version__,
|
|
83
100
|
environ=environ,
|
|
84
101
|
verbose=verbose,
|
|
102
|
+
reuse_only=reuse_only,
|
|
85
103
|
)
|
|
86
104
|
except RuntimeInstallError as exc:
|
|
87
105
|
print(f"error: {exc}", file=sys.stderr)
|
|
@@ -166,7 +184,7 @@ def main(
|
|
|
166
184
|
return 0 if runtime is not None else 2
|
|
167
185
|
|
|
168
186
|
if runtime is None:
|
|
169
|
-
root = managed_root(environ
|
|
187
|
+
root = managed_root(environ)
|
|
170
188
|
print(
|
|
171
189
|
"Railmux for Windows needs its private MSYS2/tmux runtime.\n"
|
|
172
190
|
"The Windows Codex/Claude executables and their existing session "
|
|
@@ -175,7 +193,10 @@ def main(
|
|
|
175
193
|
)
|
|
176
194
|
if root is not None:
|
|
177
195
|
print(f"Managed location: {root}", file=sys.stderr)
|
|
178
|
-
if
|
|
196
|
+
if (
|
|
197
|
+
not stdin_isatty()
|
|
198
|
+
and reusable_managed_base_candidate(environ) is None
|
|
199
|
+
):
|
|
179
200
|
print(
|
|
180
201
|
"Run 'railmux runtime install' interactively or "
|
|
181
202
|
"'railmux runtime install --yes' to install it.",
|