Add timeout command support for macOS (#108)
* feat(timeout): add cross-platform timeout support for macOS Add portable timeout wrapper that automatically detects and uses the appropriate timeout command based on the platform: - Linux: Uses standard GNU `timeout` from coreutils - macOS: Uses `gtimeout` from Homebrew coreutils Changes: - Add lib/timeout_utils.sh with detect_timeout_command() and portable_timeout() functions - Update ralph_loop.sh to source timeout_utils.sh and use portable_timeout for Claude Code execution - Update install.sh to check for coreutils on macOS and provide installation instructions - Update test mocks to include gtimeout and portable_timeout - Update README.md with macOS coreutils installation instructions - Update CLAUDE.md with timeout_utils.sh documentation Users on macOS now need to install coreutils: brew install coreutils * Update model reference in opencode-review workflow * Update model name in opencode-review workflow * Update model version in opencode-review workflow * Update model version in opencode-review workflow * Update lib/timeout_utils.sh Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com> * Update opencode-review.yml --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>
This commit is contained in:
parent
509a9699a8
commit
0e95f67318
6 changed files with 237 additions and 14 deletions
|
|
@ -52,6 +52,13 @@ The system uses a modular architecture with reusable components in the `lib/` di
|
|||
- ISO timestamp generation for logging
|
||||
- Epoch time calculations for rate limiting
|
||||
|
||||
4. **lib/timeout_utils.sh** - Cross-platform timeout command utilities
|
||||
- Detects and uses appropriate timeout command for the platform
|
||||
- Linux: Uses standard `timeout` from GNU coreutils
|
||||
- macOS: Uses `gtimeout` from Homebrew coreutils
|
||||
- `portable_timeout()` function for seamless cross-platform execution
|
||||
- Automatic detection with caching for performance
|
||||
|
||||
## Key Commands
|
||||
|
||||
### Installation
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -471,6 +471,9 @@ my-project/
|
|||
- **tmux** - Terminal multiplexer for integrated monitoring (recommended)
|
||||
- **jq** - JSON processing for status tracking
|
||||
- **Git** - Version control (projects are initialized as git repos)
|
||||
- **GNU coreutils** - For the `timeout` command (execution timeouts)
|
||||
- Linux: Pre-installed on most distributions
|
||||
- macOS: Install via `brew install coreutils` (provides `gtimeout`)
|
||||
- **Standard Unix tools** - grep, date, etc.
|
||||
|
||||
### Testing Requirements (Development)
|
||||
|
|
@ -524,6 +527,20 @@ brew install tmux
|
|||
sudo yum install tmux
|
||||
```
|
||||
|
||||
### Installing GNU coreutils (macOS)
|
||||
|
||||
Ralph uses the `timeout` command for execution timeouts. On macOS, you need to install GNU coreutils:
|
||||
|
||||
```bash
|
||||
# Install coreutils (provides gtimeout)
|
||||
brew install coreutils
|
||||
|
||||
# Verify installation
|
||||
gtimeout --version
|
||||
```
|
||||
|
||||
Ralph automatically detects and uses `gtimeout` on macOS. No additional configuration is required after installation.
|
||||
|
||||
## Monitoring and Debugging
|
||||
|
||||
### Live Dashboard
|
||||
|
|
@ -569,6 +586,7 @@ tail -f logs/ralph.log
|
|||
- **Missing Dependencies** - Ensure Claude Code CLI and tmux are installed
|
||||
- **tmux Session Lost** - Use `tmux list-sessions` and `tmux attach` to reconnect
|
||||
- **Session Expired** - Sessions expire after 24 hours by default; use `--reset-session` to start fresh
|
||||
- **timeout: command not found (macOS)** - Install GNU coreutils: `brew install coreutils`
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
46
install.sh
46
install.sh
|
|
@ -33,38 +33,62 @@ log() {
|
|||
# Check dependencies
|
||||
check_dependencies() {
|
||||
log "INFO" "Checking dependencies..."
|
||||
|
||||
|
||||
local missing_deps=()
|
||||
|
||||
local os_type
|
||||
os_type=$(uname)
|
||||
|
||||
if ! command -v node &> /dev/null && ! command -v npx &> /dev/null; then
|
||||
missing_deps+=("Node.js/npm")
|
||||
fi
|
||||
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
missing_deps+=("jq")
|
||||
fi
|
||||
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
missing_deps+=("git")
|
||||
fi
|
||||
|
||||
|
||||
# Check for timeout command (platform-specific)
|
||||
if [[ "$os_type" == "Darwin" ]]; then
|
||||
# macOS: check for gtimeout from coreutils
|
||||
if ! command -v gtimeout &> /dev/null && ! command -v timeout &> /dev/null; then
|
||||
missing_deps+=("coreutils (for timeout command)")
|
||||
fi
|
||||
else
|
||||
# Linux: check for standard timeout command
|
||||
if ! command -v timeout &> /dev/null; then
|
||||
missing_deps+=("coreutils")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ${#missing_deps[@]} -ne 0 ]; then
|
||||
log "ERROR" "Missing required dependencies: ${missing_deps[*]}"
|
||||
echo "Please install the missing dependencies:"
|
||||
echo " Ubuntu/Debian: sudo apt-get install nodejs npm jq git"
|
||||
echo " macOS: brew install node jq git"
|
||||
echo " CentOS/RHEL: sudo yum install nodejs npm jq git"
|
||||
echo " Ubuntu/Debian: sudo apt-get install nodejs npm jq git coreutils"
|
||||
echo " macOS: brew install node jq git coreutils"
|
||||
echo " CentOS/RHEL: sudo yum install nodejs npm jq git coreutils"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Additional macOS-specific warning for coreutils
|
||||
if [[ "$os_type" == "Darwin" ]]; then
|
||||
if command -v gtimeout &> /dev/null; then
|
||||
log "INFO" "GNU coreutils detected (gtimeout available)"
|
||||
elif command -v timeout &> /dev/null; then
|
||||
log "INFO" "timeout command available"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Claude Code CLI will be downloaded automatically when first used
|
||||
log "INFO" "Claude Code CLI (@anthropic-ai/claude-code) will be downloaded when first used."
|
||||
|
||||
|
||||
# Check tmux (optional)
|
||||
if ! command -v tmux &> /dev/null; then
|
||||
log "WARN" "tmux not found. Install for integrated monitoring: apt-get install tmux / brew install tmux"
|
||||
fi
|
||||
|
||||
|
||||
log "SUCCESS" "Dependencies check completed"
|
||||
}
|
||||
|
||||
|
|
|
|||
145
lib/timeout_utils.sh
Executable file
145
lib/timeout_utils.sh
Executable file
|
|
@ -0,0 +1,145 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# timeout_utils.sh - Cross-platform timeout utility functions
|
||||
# Provides consistent timeout command execution across GNU (Linux) and BSD (macOS) systems
|
||||
#
|
||||
# On Linux: Uses the built-in GNU `timeout` command from coreutils
|
||||
# On macOS: Uses `gtimeout` from Homebrew coreutils, or falls back to `timeout` if available
|
||||
|
||||
# Cached timeout command to avoid repeated detection
|
||||
export _TIMEOUT_CMD=""
|
||||
|
||||
# Detect the available timeout command for this platform
|
||||
# Sets _TIMEOUT_CMD to the appropriate command
|
||||
# Returns 0 if a timeout command is available, 1 if not
|
||||
detect_timeout_command() {
|
||||
# Return cached result if already detected
|
||||
if [[ -n "$_TIMEOUT_CMD" ]]; then
|
||||
echo "$_TIMEOUT_CMD"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local os_type
|
||||
os_type=$(uname)
|
||||
|
||||
if [[ "$os_type" == "Darwin" ]]; then
|
||||
# macOS: Check for gtimeout (from Homebrew coreutils) first
|
||||
if command -v gtimeout &> /dev/null; then
|
||||
_TIMEOUT_CMD="gtimeout"
|
||||
elif command -v timeout &> /dev/null; then
|
||||
# Some macOS setups might have timeout available (e.g., MacPorts)
|
||||
_TIMEOUT_CMD="timeout"
|
||||
else
|
||||
# No timeout command available
|
||||
_TIMEOUT_CMD=""
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Linux and other Unix systems: use standard timeout
|
||||
if command -v timeout &> /dev/null; then
|
||||
_TIMEOUT_CMD="timeout"
|
||||
else
|
||||
# Timeout not found (unusual on Linux)
|
||||
_TIMEOUT_CMD=""
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$_TIMEOUT_CMD"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check if a timeout command is available on this system
|
||||
# Returns 0 if available, 1 if not
|
||||
has_timeout_command() {
|
||||
local cmd
|
||||
cmd=$(detect_timeout_command 2>/dev/null)
|
||||
[[ -n "$cmd" ]]
|
||||
}
|
||||
|
||||
# Get a user-friendly message about timeout availability
|
||||
# Useful for error messages and installation instructions
|
||||
get_timeout_status_message() {
|
||||
local os_type
|
||||
os_type=$(uname)
|
||||
|
||||
if has_timeout_command; then
|
||||
local cmd
|
||||
cmd=$(detect_timeout_command)
|
||||
echo "Timeout command available: $cmd"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$os_type" == "Darwin" ]]; then
|
||||
echo "Timeout command not found. Install GNU coreutils: brew install coreutils"
|
||||
else
|
||||
echo "Timeout command not found. Install coreutils: sudo apt-get install coreutils"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Execute a command with a timeout (cross-platform)
|
||||
# Usage: portable_timeout DURATION COMMAND [ARGS...]
|
||||
#
|
||||
# Arguments:
|
||||
# DURATION - Timeout duration (e.g., "30s", "5m", "1h")
|
||||
# COMMAND - The command to execute
|
||||
# ARGS - Additional arguments for the command
|
||||
#
|
||||
# Returns:
|
||||
# 0 - Command completed successfully within timeout
|
||||
# 124 - Command timed out (GNU timeout behavior)
|
||||
# 1 - No timeout command available (logs error)
|
||||
# * - Exit code from the executed command
|
||||
#
|
||||
# Example:
|
||||
# portable_timeout 30s curl -s https://example.com
|
||||
# portable_timeout 5m npm install
|
||||
#
|
||||
portable_timeout() {
|
||||
local duration=$1
|
||||
shift
|
||||
|
||||
# Validate arguments
|
||||
if [[ -z "$duration" ]]; then
|
||||
echo "Error: portable_timeout requires a duration argument" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Error: portable_timeout requires a command to execute" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Detect the timeout command
|
||||
local timeout_cmd
|
||||
timeout_cmd=$(detect_timeout_command 2>/dev/null)
|
||||
|
||||
if [[ -z "$timeout_cmd" ]]; then
|
||||
local os_type
|
||||
os_type=$(uname)
|
||||
|
||||
echo "Error: No timeout command available on this system" >&2
|
||||
if [[ "$os_type" == "Darwin" ]]; then
|
||||
echo "Install GNU coreutils on macOS: brew install coreutils" >&2
|
||||
else
|
||||
echo "Install coreutils: sudo apt-get install coreutils" >&2
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Execute the command with timeout
|
||||
"$timeout_cmd" "$duration" "$@"
|
||||
}
|
||||
|
||||
# Reset the cached timeout command (useful for testing)
|
||||
reset_timeout_detection() {
|
||||
_TIMEOUT_CMD=""
|
||||
}
|
||||
|
||||
# Export functions for use in other scripts
|
||||
export -f detect_timeout_command
|
||||
export -f has_timeout_command
|
||||
export -f get_timeout_status_message
|
||||
export -f portable_timeout
|
||||
export -f reset_timeout_detection
|
||||
|
|
@ -8,6 +8,7 @@ set -e # Exit on any error
|
|||
# Source library components
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
source "$SCRIPT_DIR/lib/date_utils.sh"
|
||||
source "$SCRIPT_DIR/lib/timeout_utils.sh"
|
||||
source "$SCRIPT_DIR/lib/response_analyzer.sh"
|
||||
source "$SCRIPT_DIR/lib/circuit_breaker.sh"
|
||||
|
||||
|
|
@ -866,7 +867,7 @@ execute_claude_code() {
|
|||
if [[ "$use_modern_cli" == "true" ]]; then
|
||||
# Modern execution with command array (shell-injection safe)
|
||||
# Execute array directly without bash -c to prevent shell metacharacter interpretation
|
||||
if timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" > "$output_file" 2>&1 &
|
||||
if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" > "$output_file" 2>&1 &
|
||||
then
|
||||
: # Continue to wait loop
|
||||
else
|
||||
|
|
@ -879,7 +880,7 @@ execute_claude_code() {
|
|||
|
||||
# Fall back to legacy stdin piping if modern mode failed or not enabled
|
||||
if [[ "$use_modern_cli" == "false" ]]; then
|
||||
if timeout ${timeout_seconds}s $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>&1 &
|
||||
if portable_timeout ${timeout_seconds}s $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>&1 &
|
||||
then
|
||||
: # Continue to wait loop
|
||||
else
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ mock_stat() {
|
|||
return 0
|
||||
}
|
||||
|
||||
# Mock timeout command
|
||||
# Mock timeout command (Linux)
|
||||
mock_timeout() {
|
||||
local duration=$1
|
||||
shift
|
||||
|
|
@ -216,6 +216,28 @@ mock_timeout() {
|
|||
return $?
|
||||
}
|
||||
|
||||
# Mock gtimeout command (macOS coreutils)
|
||||
# Same behavior as timeout - both are GNU coreutils timeout commands
|
||||
mock_gtimeout() {
|
||||
local duration=$1
|
||||
shift
|
||||
|
||||
# Execute the command without actual timeout
|
||||
"$@"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Mock portable_timeout (cross-platform wrapper from timeout_utils.sh)
|
||||
# This mock bypasses the actual timeout detection and just executes the command
|
||||
mock_portable_timeout() {
|
||||
local duration=$1
|
||||
shift
|
||||
|
||||
# Execute the command without actual timeout
|
||||
"$@"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Setup all mocks
|
||||
setup_mocks() {
|
||||
# Replace system commands with mocks
|
||||
|
|
@ -225,6 +247,8 @@ setup_mocks() {
|
|||
function notify-send() { mock_notify_send "$@"; }
|
||||
function osascript() { mock_osascript "$@"; }
|
||||
function timeout() { mock_timeout "$@"; }
|
||||
function gtimeout() { mock_gtimeout "$@"; }
|
||||
function portable_timeout() { mock_portable_timeout "$@"; }
|
||||
|
||||
export -f claude
|
||||
export -f tmux
|
||||
|
|
@ -232,6 +256,8 @@ setup_mocks() {
|
|||
export -f notify-send
|
||||
export -f osascript
|
||||
export -f timeout
|
||||
export -f gtimeout
|
||||
export -f portable_timeout
|
||||
}
|
||||
|
||||
# Teardown all mocks
|
||||
|
|
@ -242,6 +268,8 @@ teardown_mocks() {
|
|||
unset -f notify-send
|
||||
unset -f osascript
|
||||
unset -f timeout
|
||||
unset -f gtimeout
|
||||
unset -f portable_timeout
|
||||
}
|
||||
|
||||
# Set mock behavior
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue