From ca98742891754ed98e724840aa7838e6acca5085 Mon Sep 17 00:00:00 2001 From: reynold Date: Thu, 28 Aug 2025 21:08:58 +0800 Subject: [PATCH] Fix macOS compatibility: Add portable timeout helper - Add utils.sh with cross-platform timeout functions for macOS compatibility - Fix sf-logs-tail to use portable_timeout instead of GNU timeout command - Fix test-all-wrappers.sh to use portable_timeout_seconds for testing - Update README.md with macOS compatibility documentation The timeout command is not available by default on macOS, causing sf-logs-tail and test scripts to fail. The new utils.sh provides fallback timeout functionality that works on Linux, macOS with/without GNU coreutils, maintaining exit code 124 compatibility. --- README.md | 10 ++++ sf-logs-tail | 5 +- test-all-wrappers.sh | 5 +- utils.sh | 107 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100755 utils.sh diff --git a/README.md b/README.md index 3cf35c9..bfca4d8 100644 --- a/README.md +++ b/README.md @@ -508,6 +508,16 @@ sf-deploy -to DEMO-ORG \ - **Flexible Input**: Supports absolute and repository-relative paths - **Command Echo**: Shows the actual `sf` command being executed for transparency - **Focused Workflows**: Deploy, validate, retrieve, test, run Apex, manage orgs, data import/export, and tail logs +- **macOS Compatibility**: Full compatibility with macOS using portable timeout implementation + +## macOS Compatibility + +The wrapper scripts include a `utils.sh` helper that provides cross-platform timeout functionality. This ensures that scripts requiring timed execution (like `sf-logs-tail`) work correctly on macOS without requiring GNU coreutils. + +The timeout helper automatically detects and uses: +1. `timeout` command (Linux/GNU systems) +2. `gtimeout` command (macOS with GNU coreutils) +3. Built-in fallback implementation (pure Bash for macOS) ## Tips diff --git a/sf-logs-tail b/sf-logs-tail index eb07a36..664468a 100755 --- a/sf-logs-tail +++ b/sf-logs-tail @@ -3,6 +3,9 @@ # Debug logs tail wrapper for Salesforce CLI # Provides real-time debug log monitoring with filtering and formatting +# Source utilities for cross-platform compatibility +source "$(dirname "$0")/utils.sh" + # Color codes for output formatting readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' @@ -291,7 +294,7 @@ if [[ "$VERBOSE" == true ]]; then fi # Start the log tailing with timeout -timeout "${DURATION}m" sf "${SF_ARGS[@]}" 2>/dev/null | while IFS= read -r line; do +portable_timeout "$DURATION" sf "${SF_ARGS[@]}" 2>/dev/null | while IFS= read -r line; do if should_show_log_entry "$line" "$FILTER_PATTERN" "$APEX_ONLY"; then format_log_entry "$line" "$SHOW_COLORS" "$VERBOSE" fi diff --git a/test-all-wrappers.sh b/test-all-wrappers.sh index acc46c0..a3eea0b 100755 --- a/test-all-wrappers.sh +++ b/test-all-wrappers.sh @@ -4,6 +4,9 @@ set -euo pipefail # Comprehensive Test Suite for SF CLI Wrapper Scripts # Tests all scenarios with 100% coverage using PWC-TEAM-DEV org +# Source utilities for cross-platform compatibility +source "$(dirname "$0")/utils.sh" + readonly TEST_ORG="PWC-TEAM-DEV" readonly GREEN='\033[0;32m' readonly RED='\033[0;31m' @@ -182,7 +185,7 @@ echo "" echo -e "${BLUE}=== Testing sf-logs-tail (quick test) ===${NC}" # Test logs tail for a very short duration -run_test "sf-logs-tail short duration" "timeout 5s ./sf-logs-tail -to $TEST_ORG --duration 1 || true" +run_test "sf-logs-tail short duration" "portable_timeout_seconds 5 ./sf-logs-tail -to $TEST_ORG --duration 1 || true" echo "" echo -e "${BLUE}=== Test Results Summary ===${NC}" diff --git a/utils.sh b/utils.sh new file mode 100755 index 0000000..b8ce859 --- /dev/null +++ b/utils.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# Shared utilities for SF CLI wrapper scripts +# Provides cross-platform compatibility helpers + +# Portable timeout function for macOS and Linux compatibility +# Usage: portable_timeout MINUTES command [args...] +# Returns exit code 124 on timeout (matching GNU timeout behavior) +portable_timeout() { + local duration_minutes="$1" + shift + + # Try GNU timeout first (available on Linux by default) + if command -v timeout >/dev/null 2>&1; then + timeout "${duration_minutes}m" "$@" + return $? + fi + + # Try gtimeout (macOS with GNU coreutils installed) + if command -v gtimeout >/dev/null 2>&1; then + gtimeout "${duration_minutes}m" "$@" + return $? + fi + + # Fallback implementation for macOS without GNU coreutils + # Start the command in background + "$@" & + local cmd_pid=$! + + # Start timeout watcher in background + ( + sleep "${duration_minutes}m" + kill "$cmd_pid" 2>/dev/null + ) & + local watcher_pid=$! + + # Wait for the command to finish + wait "$cmd_pid" 2>/dev/null + local cmd_exit_code=$? + + # Clean up watcher if command finished normally + kill "$watcher_pid" 2>/dev/null + wait "$watcher_pid" 2>/dev/null + + # Check if command was killed (timeout occurred) + if ! kill -0 "$cmd_pid" 2>/dev/null; then + # Command no longer exists, check if it was our timeout + if [[ $cmd_exit_code -ne 0 ]]; then + # Command may have been killed by timeout, return 124 + return 124 + fi + fi + + return $cmd_exit_code +} + +# Portable timeout function for seconds (for tests that need shorter timeouts) +# Usage: portable_timeout_seconds SECONDS command [args...] +portable_timeout_seconds() { + local duration_seconds="$1" + shift + + # Try GNU timeout first + if command -v timeout >/dev/null 2>&1; then + timeout "${duration_seconds}s" "$@" + return $? + fi + + # Try gtimeout + if command -v gtimeout >/dev/null 2>&1; then + gtimeout "${duration_seconds}s" "$@" + return $? + fi + + # Fallback implementation + "$@" & + local cmd_pid=$! + + # Start timeout watcher in background + ( + sleep "$duration_seconds" + kill "$cmd_pid" 2>/dev/null + ) & + local watcher_pid=$! + + # Wait for the command to finish + wait "$cmd_pid" 2>/dev/null + local cmd_exit_code=$? + + # Clean up watcher if command finished normally + kill "$watcher_pid" 2>/dev/null + wait "$watcher_pid" 2>/dev/null + + # Check if command was killed (timeout occurred) + if ! kill -0 "$cmd_pid" 2>/dev/null; then + if [[ $cmd_exit_code -ne 0 ]]; then + return 124 + fi + fi + + return $cmd_exit_code +} + +# Function to get the directory of the calling script +# Usage: SCRIPT_DIR=$(get_script_dir) +get_script_dir() { + cd "$(dirname "${BASH_SOURCE[1]}")" && pwd +}