railmux 0.4.0.dev14__tar.gz → 0.4.0.dev15__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.dev14/src/railmux.egg-info → railmux-0.4.0.dev15}/PKG-INFO +1 -1
- railmux-0.4.0.dev15/src/railmux/__init__.py +1 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/cli.py +177 -25
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/diagnostics.py +2 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/tmux_health.py +2 -0
- railmux-0.4.0.dev15/src/railmux/windows_attach_relay.py +789 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux.egg-info/SOURCES.txt +2 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_cli.py +113 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_tmux_integration.py +75 -0
- railmux-0.4.0.dev15/tests/test_windows_attach_relay.py +271 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_windows_msys2.py +1 -1
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_windows_wrapper_contract.py +1 -0
- railmux-0.4.0.dev14/src/railmux/__init__.py +0 -1
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/LICENSE +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/MANIFEST.in +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/README.md +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/pyproject.toml +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/setup.cfg +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/__main__.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/background_index.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/config.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/discovery.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/favorites.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/launcher.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/local_open.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/models.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/modes.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/renames.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/self_update.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/session_index.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/settings.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/transcript.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_background_index.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_codex_index.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_config.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_config_cli.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_discovery.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_display_transport.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_fast_full_window.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_help_agent.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_keymap.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_launcher.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_local_open.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_modals.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_modes.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_new_project.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_options.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_path_codec.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_release_notes.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_remote_config.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_renames.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_restart_state.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_running_pane.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_running_sort.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_self_update.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_session_cache.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_session_index.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_settings.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_sidebar.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_soft_quit.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_status.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_statusbar.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_system_deps.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_transcript.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_widgets.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tools/release_notes.py +0 -0
- {railmux-0.4.0.dev14 → railmux-0.4.0.dev15}/tools/wheel_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0.dev15"
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import os
|
|
4
5
|
import shutil
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
7
8
|
import termios
|
|
9
|
+
import threading
|
|
8
10
|
import time
|
|
11
|
+
import tty
|
|
9
12
|
from dataclasses import replace
|
|
10
13
|
from pathlib import Path
|
|
11
14
|
|
|
@@ -50,7 +53,20 @@ def _restore_terminal(attributes: list | None) -> None:
|
|
|
50
53
|
pass
|
|
51
54
|
|
|
52
55
|
|
|
53
|
-
def
|
|
56
|
+
def _reset_terminal_modes(fd: int) -> None:
|
|
57
|
+
"""Best-effort recovery when a relay ends before tmux's restore tail."""
|
|
58
|
+
try:
|
|
59
|
+
os.write(
|
|
60
|
+
fd,
|
|
61
|
+
b"\x1b[?1049l\x1b[?1l\x1b>\x1b[?1000l\x1b[?1002l"
|
|
62
|
+
b"\x1b[?1003l\x1b[?1004l\x1b[?1006l\x1b[?2004l"
|
|
63
|
+
b"\x1b[?7h\x1b[?25h\x1b[0m\r",
|
|
64
|
+
)
|
|
65
|
+
except OSError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _stop_tmux_client(process: object) -> None:
|
|
54
70
|
if process.poll() is not None:
|
|
55
71
|
return
|
|
56
72
|
process.terminate()
|
|
@@ -75,20 +91,43 @@ def _run_tmux_client_with_watchdog(
|
|
|
75
91
|
attributes = termios.tcgetattr(sys.stdin.fileno())
|
|
76
92
|
except (OSError, termios.error):
|
|
77
93
|
pass
|
|
94
|
+
started_at = time.monotonic()
|
|
78
95
|
try:
|
|
79
|
-
process = subprocess.Popen(argv, env=env)
|
|
96
|
+
process: object = subprocess.Popen(argv, env=env)
|
|
80
97
|
except OSError as exc:
|
|
81
98
|
print(f"error: could not start tmux client: {exc}", file=sys.stderr)
|
|
82
99
|
return 2
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
def monitor_client(
|
|
101
|
+
monitor_started_at: float | None = None,
|
|
102
|
+
*,
|
|
103
|
+
asynchronous_probe: bool = False,
|
|
104
|
+
) -> tuple[int, bool]:
|
|
105
|
+
nonlocal expected_target, expected_session_id
|
|
106
|
+
watchdog = tmux_health.FailureWatchdog.starting(
|
|
107
|
+
(
|
|
108
|
+
time.monotonic()
|
|
109
|
+
if monitor_started_at is None
|
|
110
|
+
else monitor_started_at
|
|
111
|
+
),
|
|
112
|
+
interval=_LOCAL_WATCHDOG_INTERVAL,
|
|
113
|
+
failure_limit=_LOCAL_WATCHDOG_FAILURES,
|
|
114
|
+
)
|
|
115
|
+
next_session_probe = watchdog.next_probe - watchdog.interval
|
|
116
|
+
probe_thread: threading.Thread | None = None
|
|
117
|
+
probe_result: list[tmux_server.TmuxServerTarget | None] = []
|
|
118
|
+
|
|
119
|
+
def discover() -> tmux_server.TmuxServerTarget | None:
|
|
120
|
+
try:
|
|
121
|
+
return tmux_server.discover_target(timeout=1.0)
|
|
122
|
+
except tmux_server.TmuxServerError:
|
|
123
|
+
return None
|
|
124
|
+
|
|
90
125
|
while process.poll() is None:
|
|
91
|
-
|
|
126
|
+
pump = getattr(process, "pump", None)
|
|
127
|
+
if pump is None:
|
|
128
|
+
time.sleep(0.25)
|
|
129
|
+
else:
|
|
130
|
+
pump(0.25)
|
|
92
131
|
now = time.monotonic()
|
|
93
132
|
if (
|
|
94
133
|
expected_target is not None
|
|
@@ -100,10 +139,26 @@ def _run_tmux_client_with_watchdog(
|
|
|
100
139
|
next_session_probe = now + 1.0
|
|
101
140
|
if not watchdog.due(now):
|
|
102
141
|
continue
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
142
|
+
if asynchronous_probe:
|
|
143
|
+
if probe_thread is None:
|
|
144
|
+
probe_result.clear()
|
|
145
|
+
|
|
146
|
+
def run_probe() -> None:
|
|
147
|
+
probe_result.append(discover())
|
|
148
|
+
|
|
149
|
+
probe_thread = threading.Thread(
|
|
150
|
+
target=run_probe,
|
|
151
|
+
name="railmux-tmux-health",
|
|
152
|
+
daemon=True,
|
|
153
|
+
)
|
|
154
|
+
probe_thread.start()
|
|
155
|
+
continue
|
|
156
|
+
if probe_thread.is_alive():
|
|
157
|
+
continue
|
|
158
|
+
current_target = probe_result[0] if probe_result else None
|
|
159
|
+
probe_thread = None
|
|
160
|
+
else:
|
|
161
|
+
current_target = discover()
|
|
107
162
|
if expected_target is None and current_target is not None:
|
|
108
163
|
expected_target = current_target
|
|
109
164
|
expected_session_id = tmux_server.target_session_id(
|
|
@@ -126,23 +181,94 @@ def _run_tmux_client_with_watchdog(
|
|
|
126
181
|
"responding; run 'railmux doctor' for diagnostics",
|
|
127
182
|
file=sys.stderr,
|
|
128
183
|
)
|
|
129
|
-
return 2
|
|
184
|
+
return 2, True
|
|
185
|
+
return process.returncode or 0, False
|
|
186
|
+
|
|
187
|
+
relay = None
|
|
188
|
+
relay_attempted = False
|
|
189
|
+
try:
|
|
190
|
+
returncode, watchdog_failed = monitor_client(started_at)
|
|
191
|
+
if watchdog_failed:
|
|
192
|
+
return returncode
|
|
193
|
+
failed_at = time.monotonic()
|
|
130
194
|
# tmux may leave the client TTY in raw mode even after its process
|
|
131
195
|
# exits. Restore it before a bounded recovery probe or user message.
|
|
132
196
|
_restore_terminal(attributes)
|
|
133
|
-
|
|
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
|
-
)
|
|
197
|
+
current_target = None
|
|
140
198
|
if returncode and expected_target is not None:
|
|
141
199
|
try:
|
|
142
200
|
current_target = tmux_server.discover_target(timeout=1.0)
|
|
143
201
|
except tmux_server.TmuxServerError:
|
|
144
202
|
current_target = None
|
|
145
|
-
if current_target
|
|
203
|
+
if current_target == expected_target and expected_session_id is None:
|
|
204
|
+
expected_session_id = tmux_server.target_session_id(
|
|
205
|
+
expected_target, "railmux", timeout=0.5)
|
|
206
|
+
|
|
207
|
+
from railmux.provider_paths import running_in_managed_windows_wrapper
|
|
208
|
+
|
|
209
|
+
managed_windows = running_in_managed_windows_wrapper()
|
|
210
|
+
|
|
211
|
+
can_relay = bool(
|
|
212
|
+
current_target == expected_target
|
|
213
|
+
and expected_session_id is not None
|
|
214
|
+
and failed_at - started_at < 5.0
|
|
215
|
+
and attributes is not None
|
|
216
|
+
and sys.stdout.isatty()
|
|
217
|
+
and managed_windows
|
|
218
|
+
)
|
|
219
|
+
if can_relay:
|
|
220
|
+
relay_attempted = True
|
|
221
|
+
from railmux.windows_attach_relay import (
|
|
222
|
+
WindowsAttachRelayError,
|
|
223
|
+
start_relay_client,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
print(
|
|
227
|
+
"info: direct tmux attach was unavailable; reconnecting "
|
|
228
|
+
"through the Windows terminal bridge",
|
|
229
|
+
file=sys.stderr,
|
|
230
|
+
)
|
|
231
|
+
try:
|
|
232
|
+
tty.setraw(sys.stdin.fileno())
|
|
233
|
+
relay = start_relay_client(
|
|
234
|
+
target=expected_target,
|
|
235
|
+
session_id=expected_session_id,
|
|
236
|
+
environ=env,
|
|
237
|
+
stdin_fd=sys.stdin.fileno(),
|
|
238
|
+
stdout_fd=sys.stdout.fileno(),
|
|
239
|
+
)
|
|
240
|
+
process = relay
|
|
241
|
+
returncode, watchdog_failed = monitor_client(
|
|
242
|
+
asynchronous_probe=True)
|
|
243
|
+
if returncode:
|
|
244
|
+
_reset_terminal_modes(sys.stdout.fileno())
|
|
245
|
+
if watchdog_failed:
|
|
246
|
+
return returncode
|
|
247
|
+
except (OSError, termios.error, WindowsAttachRelayError):
|
|
248
|
+
_reset_terminal_modes(sys.stdout.fileno())
|
|
249
|
+
_restore_terminal(attributes)
|
|
250
|
+
outcome = (
|
|
251
|
+
"could not attach"
|
|
252
|
+
if relay is None
|
|
253
|
+
else "connection ended unexpectedly"
|
|
254
|
+
)
|
|
255
|
+
print(
|
|
256
|
+
f"error: the Windows terminal bridge {outcome}; the "
|
|
257
|
+
"existing workspace was left running; run "
|
|
258
|
+
"'railmux doctor' for diagnostics",
|
|
259
|
+
file=sys.stderr,
|
|
260
|
+
)
|
|
261
|
+
returncode = 2
|
|
262
|
+
finally:
|
|
263
|
+
_restore_terminal(attributes)
|
|
264
|
+
|
|
265
|
+
if returncode:
|
|
266
|
+
try:
|
|
267
|
+
current_target = tmux_server.discover_target(timeout=1.0)
|
|
268
|
+
except tmux_server.TmuxServerError:
|
|
269
|
+
current_target = None
|
|
270
|
+
|
|
271
|
+
if returncode and current_target != expected_target:
|
|
146
272
|
clean_exit = bool(
|
|
147
273
|
expected_session_id is not None
|
|
148
274
|
and tmux_health.consume_clean_exit(
|
|
@@ -156,16 +282,41 @@ def _run_tmux_client_with_watchdog(
|
|
|
156
282
|
reason="launcher-server-exit",
|
|
157
283
|
consecutive_failures=1,
|
|
158
284
|
)
|
|
285
|
+
elif (
|
|
286
|
+
returncode
|
|
287
|
+
and current_target == expected_target
|
|
288
|
+
and managed_windows
|
|
289
|
+
):
|
|
290
|
+
tmux_health.record_incident(
|
|
291
|
+
component="launcher",
|
|
292
|
+
reason=(
|
|
293
|
+
"launcher-relay-failed"
|
|
294
|
+
if relay_attempted
|
|
295
|
+
else "launcher-attach-rejected"
|
|
296
|
+
),
|
|
297
|
+
consecutive_failures=1,
|
|
298
|
+
)
|
|
299
|
+
# Routine post-exit removal of a proof-authorized last-session socket
|
|
300
|
+
# is housekeeping. Only pre-launch recovery is user-visible.
|
|
301
|
+
windows_tmux_lifecycle.recover_abandoned_socket()
|
|
159
302
|
return returncode
|
|
160
303
|
except KeyboardInterrupt:
|
|
161
304
|
_stop_tmux_client(process)
|
|
305
|
+
if relay is not None:
|
|
306
|
+
_reset_terminal_modes(sys.stdout.fileno())
|
|
162
307
|
return 130
|
|
163
308
|
finally:
|
|
309
|
+
if relay is not None:
|
|
310
|
+
relay.close()
|
|
164
311
|
_restore_terminal(attributes)
|
|
165
312
|
|
|
166
313
|
|
|
167
314
|
def main(argv: list[str] | None = None) -> int:
|
|
168
315
|
raw_args = list(sys.argv[1:] if argv is None else argv)
|
|
316
|
+
if raw_args and raw_args[0] == "_windows-attach-relay":
|
|
317
|
+
from railmux.windows_attach_relay import relay_server_main
|
|
318
|
+
|
|
319
|
+
return relay_server_main(raw_args[1:])
|
|
169
320
|
if raw_args and raw_args[0] == "config":
|
|
170
321
|
from railmux.config_cli import main as config_main
|
|
171
322
|
|
|
@@ -379,8 +530,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
379
530
|
tmux_server.socket_label()
|
|
380
531
|
if windows_tmux_lifecycle.recover_abandoned_socket():
|
|
381
532
|
print(
|
|
382
|
-
"info:
|
|
383
|
-
"Claude session files were not
|
|
533
|
+
"info: recovered an unresponsive Windows tmux socket left by "
|
|
534
|
+
"a previous session; Codex and Claude session files were not "
|
|
535
|
+
"changed",
|
|
384
536
|
file=sys.stderr,
|
|
385
537
|
)
|
|
386
538
|
dedicated_target = tmux_server.discover_target()
|
|
@@ -426,6 +426,8 @@ def _incident_text(diagnostic: IncidentDiagnostic) -> str:
|
|
|
426
426
|
if diagnostic.status == "none":
|
|
427
427
|
return "none recorded"
|
|
428
428
|
descriptions = {
|
|
429
|
+
"launcher-attach-rejected": "Windows tmux client attach rejected",
|
|
430
|
+
"launcher-relay-failed": "Windows terminal bridge failed",
|
|
429
431
|
"launcher-watchdog-timeout": "local client watchdog timeout",
|
|
430
432
|
"launcher-server-exit": "dedicated tmux server exited",
|
|
431
433
|
"remote-display-watchdog-timeout": "SSH display watchdog timeout",
|
|
@@ -17,6 +17,8 @@ _CLEAN_EXIT_MAX_AGE = 30
|
|
|
17
17
|
_SOFT_EXIT_SCHEMA_VERSION = 1
|
|
18
18
|
_SOFT_EXIT_MAX_AGE = 30
|
|
19
19
|
_INCIDENT_REASONS = {
|
|
20
|
+
"launcher-attach-rejected",
|
|
21
|
+
"launcher-relay-failed",
|
|
20
22
|
"launcher-watchdog-timeout",
|
|
21
23
|
"launcher-server-exit",
|
|
22
24
|
"remote-display-watchdog-timeout",
|