Source code for ralph.exit_pause
"""Exit pause — decide whether to hold the terminal open before process exit.
Ported from ralph-workflow/src/exit_pause/io.rs.
"""
from __future__ import annotations
import os
import sys
from dataclasses import dataclass
from typing import TYPE_CHECKING
import psutil
from ralph.exit_pause.exit_outcome import ExitOutcome
from ralph.exit_pause.pause_on_exit_mode import PauseOnExitMode
if TYPE_CHECKING:
from collections.abc import Mapping
[docs]
@dataclass(frozen=True)
class LaunchContext:
"""Context about how Ralph was launched.
Attributes:
is_windows: Whether running on Windows.
has_terminal_session_marker: Whether a terminal session marker is present.
parent_process_name: Name of the parent process if detectable.
"""
is_windows: bool
has_terminal_session_marker: bool
parent_process_name: str | None
TERMINAL_MARKERS: list[str] = [
"WT_SESSION",
"TERM",
"MSYSTEM",
"ConEmuPID",
"ALACRITTY_LOG",
"TERM_PROGRAM",
"VSCODE_GIT_IPC_HANDLE",
]
def _has_terminal_session_marker(env: Mapping[str, str]) -> bool:
"""Check if any terminal session marker environment variable is set.
Args:
env: Environment mapping to check.
Returns:
True if any terminal marker is present and non-empty.
"""
for marker in TERMINAL_MARKERS:
value = env.get(marker)
if value is not None and value.strip():
return True
return False
def _normalize_process_name(name: str) -> str:
"""Normalize a process name for comparison."""
normalized = name.strip().lower()
if "." in normalized:
ext = normalized.rsplit(".", 1)[-1]
if ext == "exe":
return normalized
return f"{normalized}.exe"
return f"{normalized}.exe"
def _is_probably_standalone_windows_launch(ctx: LaunchContext) -> bool:
"""Check if this looks like a standalone Windows launch (e.g., from Explorer)."""
if not ctx.is_windows or ctx.has_terminal_session_marker:
return False
if ctx.parent_process_name is None:
return False
return _normalize_process_name(ctx.parent_process_name) == "explorer.exe"
[docs]
def should_pause_before_exit(
mode: PauseOnExitMode,
outcome: ExitOutcome,
launch_context: LaunchContext,
) -> bool:
"""Determine if we should pause before exiting.
Args:
mode: The pause mode setting.
outcome: The exit outcome (success/failure/interrupted).
launch_context: Information about how Ralph was launched.
Returns:
True if we should pause, False otherwise.
"""
if mode == PauseOnExitMode.NEVER:
return False
if mode == PauseOnExitMode.ALWAYS:
return True
# AUTO mode: pause on failure if launched standalone on Windows
return outcome == ExitOutcome.FAILURE and _is_probably_standalone_windows_launch(launch_context)
[docs]
def detect_launch_context(*, env: Mapping[str, str] | None = None) -> LaunchContext:
"""Detect the launch context for the current process.
Args:
env: Optional environment mapping. Uses os.environ if None.
Returns:
LaunchContext with information about how Ralph was launched.
"""
is_windows = sys.platform == "win32" or os.name == "nt"
env_map = os.environ if env is None else env
has_marker = _has_terminal_session_marker(env_map)
parent_name: str | None = None
if is_windows:
parent_name = _detect_parent_on_windows()
return LaunchContext(
is_windows=is_windows,
has_terminal_session_marker=has_marker,
parent_process_name=parent_name,
)
def _detect_parent_on_windows() -> str | None:
"""Detect parent process name using psutil."""
try:
parent = psutil.Process(os.getpid()).parent()
if parent is None:
return None
return parent.name()
except Exception:
return None
[docs]
def exit_pause(mode: PauseOnExitMode = PauseOnExitMode.AUTO) -> None:
"""Pause before exit if conditions require it.
On Windows standalone launches (e.g., double-clicked .exe), it's helpful
to pause so the user can read any error messages before the window closes.
Args:
mode: The pause mode setting (default: AUTO).
"""
ctx = detect_launch_context()
if should_pause_before_exit(mode, ExitOutcome.FAILURE, ctx):
input("Press Enter to exit...")
[docs]
def exit_with_sigint_code() -> None:
"""Exit with SIGINT exit code (130).
Called when the pipeline was interrupted by Ctrl+C and all cleanup
has completed.
"""
sys.exit(130)
__all__ = [
"ExitOutcome",
"LaunchContext",
"PauseOnExitMode",
"detect_launch_context",
"exit_pause",
"exit_with_sigint_code",
"should_pause_before_exit",
]