#!/bin/bash # -*- coding: utf-8 -*- # Timestamp: "$(date '+%Y-%m-%d %H:%M:%S') ($(whoami))" # File: ./paper/scripts/shell/watch_compile.sh # Hot-recompile script with file watching and lock management ORIG_DIR="$(pwd)" THIS_DIR="$(cd $(dirname ${BASH_SOURCE[0]}) && pwd)" # Allow override via environment variable for moved projects PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$THIS_DIR/../.." && pwd)}" # Colors for output GIT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" GRAY='\033[0;90m' GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m' NC='\033[0m' # No Color echo_info() { echo -e "${GRAY}INFO: $1${NC}"; } echo_success() { echo -e "${GREEN}SUCC: $1${NC}"; } echo_warning() { echo -e "${YELLOW}WARN: $1${NC}"; } echo_error() { echo -e "${RED}ERRO: $1${NC}"; } echo_header() { echo_info "=== $1 ==="; } # --------------------------------------- # Lock file path LOCK_FILE="$PROJECT_ROOT/.compile.lock" WATCH_PID_FILE="$PROJECT_ROOT/.watch.pid" # Function to check if compilation is locked is_locked() { if [ -f "$LOCK_FILE" ]; then local lock_pid=$(cat "$LOCK_FILE" 2>/dev/null) # Check if the process is still running if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then return 0 # Locked and process is running else # Process is dead, remove stale lock rm -f "$LOCK_FILE" return 1 fi fi return 1 # Not locked } # Function to acquire lock acquire_lock() { local max_wait=60 # Maximum seconds to wait for lock local waited=0 while is_locked; do if [ $waited -eq 0 ]; then echo_warning "Compilation in progress, waiting for lock..." fi sleep 1 waited=$((waited + 1)) if [ $waited -ge $max_wait ]; then echo_error "Timeout waiting for compilation lock" return 1 fi done # Create lock with our PID echo $$ >"$LOCK_FILE" return 0 } # Function to release lock release_lock() { if [ -f "$LOCK_FILE" ]; then local lock_pid=$(cat "$LOCK_FILE" 2>/dev/null) if [ "$lock_pid" = "$$" ]; then rm -f "$LOCK_FILE" fi fi } # Cleanup function cleanup() { echo_info "Stopping watch mode..." release_lock rm -f "$WATCH_PID_FILE" exit 0 } # Trap signals for cleanup trap cleanup EXIT INT TERM # Load configuration from YAML CONFIG_FILE="$PROJECT_ROOT/config/config_manuscript.yaml" # Function to parse YAML value get_yaml_value() { local key="$1" local file="${2:-$CONFIG_FILE}" grep "^$key:" "$file" | sed 's/^[^:]*:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' } # Function to get YAML array values get_yaml_array() { local key="$1" local file="${2:-$CONFIG_FILE}" awk "/^$key:/{flag=1; next} /^[^ ]/{flag=0} flag && /^[[:space:]]*-/" "$file" | sed 's/^[[:space:]]*-[[:space:]]*//' } # Load hot-recompile configuration # Note: YAML key uses "enabled" not "enable" HOT_RECOMPILE_ENABLED=$(awk '/^hot-recompile:/{flag=1} flag && /^[[:space:]]*enabled:/{print $2; exit}' "$CONFIG_FILE" | grep -o "true\|false") COMPILE_MODE="${1:-$(get_yaml_value "hot-recompile.mode")}" # Use arg or config COMPILE_MODE="${COMPILE_MODE:-restart}" # Default to restart if not specified STABLE_LINK=$(get_yaml_value "hot-recompile.stable_link") STABLE_LINK="${STABLE_LINK:-./01_manuscript/manuscript-latest.pdf}" # Function to compile manuscript compile_with_lock() { local compilation_start_file="$PROJECT_ROOT/.compile_start_time" # Check if compilation is running if is_locked; then local lock_pid=$(cat "$LOCK_FILE" 2>/dev/null) if [ "$COMPILE_MODE" = "restart" ]; then # Check how long compilation has been running if [ -f "$compilation_start_file" ]; then local start_time=$(cat "$compilation_start_file") local current_time=$(date +%s) local elapsed=$((current_time - start_time)) if [ $elapsed -lt 3 ]; then # Just started, kill and restart echo_warning "$(date '+%H:%M:%S') - Stopping current compilation (just started)..." kill -TERM "$lock_pid" 2>/dev/null sleep 0.5 rm -f "$LOCK_FILE" "$compilation_start_file" elif [ $elapsed -gt 15 ]; then # Taking too long, kill and restart echo_warning "$(date '+%H:%M:%S') - Stopping stuck compilation (>${elapsed}s)..." kill -TERM "$lock_pid" 2>/dev/null sleep 0.5 rm -f "$LOCK_FILE" "$compilation_start_file" else # In the middle, let it finish echo_info "$(date '+%H:%M:%S') - Waiting for current compilation to finish (${elapsed}s elapsed)..." return 1 fi fi else # Wait mode echo_warning "$(date '+%H:%M:%S') - Compilation in progress, waiting..." return 1 fi fi if acquire_lock; then echo_info "$(date '+%H:%M:%S') - Starting compilation..." date +%s >"$compilation_start_file" cd "$PROJECT_ROOT" ./compile -m local status=$? release_lock rm -f "$compilation_start_file" if [ $status -eq 0 ]; then echo_success "$(date '+%H:%M:%S') - Compilation successful" # Load configuration to get environment variables source ./config/load_config.sh manuscript >/dev/null 2>&1 # Update symlink to latest archive version (prevents viewing corrupted PDFs during compilation) local archive_dir="${SCITEX_WRITER_VERSIONS_DIR}" local latest_archive=$(ls -1 "$archive_dir"/${SCITEX_WRITER_DOC_TYPE}_v[0-9]*.pdf 2>/dev/null | grep -v "_diff.pdf" | sort -V | tail -1) if [ -n "$latest_archive" ]; then # Create relative symlink to archive cd "${SCITEX_WRITER_ROOT_DIR}" ln -sf "archive/$(basename "$latest_archive")" "${SCITEX_WRITER_DOC_TYPE}-latest.pdf" cd - >/dev/null echo_info " Symlink updated: ${SCITEX_WRITER_DOC_TYPE}-latest.pdf -> archive/$(basename "$latest_archive")" else # Fallback if no archive exists cd "${SCITEX_WRITER_ROOT_DIR}" ln -sf "${SCITEX_WRITER_DOC_TYPE}.pdf" "${SCITEX_WRITER_DOC_TYPE}-latest.pdf" cd - >/dev/null echo_info " Symlink updated: ${SCITEX_WRITER_DOC_TYPE}-latest.pdf -> ${SCITEX_WRITER_DOC_TYPE}.pdf (no archive yet)" fi else echo_error "$(date '+%H:%M:%S') - Compilation failed" fi return $status else echo_warning "$(date '+%H:%M:%S') - Could not acquire lock" return 1 fi } # Function to get list of files to watch from config get_watch_files() { # Read watch patterns from YAML config local patterns=$(awk '/^ watching_files:/,/^[^ ]/' "$CONFIG_FILE" | grep '^[[:space:]]*-' | sed 's/^[[:space:]]*-[[:space:]]*//' | sed 's/"//g') # Expand patterns and find matching files for pattern in $patterns; do # Skip comments [[ "$pattern" =~ ^# ]] && continue # Expand the pattern (handles wildcards) if [[ "$pattern" == *"**"* ]]; then # Handle recursive patterns local base_dir=$(echo "$pattern" | sed 's/\/\*\*.*//') local file_pattern=$(echo "$pattern" | sed 's/.*\*\*\///') find "$PROJECT_ROOT/$base_dir" -type f -name "$file_pattern" 2>/dev/null elif [[ "$pattern" == *"*"* ]]; then # Handle simple wildcards ls $PROJECT_ROOT/$pattern 2>/dev/null else # Direct file [ -f "$PROJECT_ROOT/$pattern" ] && echo "$PROJECT_ROOT/$pattern" fi done | sort -u } # Main watch loop main() { # Save PID for external monitoring echo $$ >"$WATCH_PID_FILE" # Check if hot-recompile is enabled if [ "$HOT_RECOMPILE_ENABLED" != "true" ]; then echo_warning "Hot-recompile is disabled in config. Set hot-recompile.enabled: true to enable." exit 0 fi # Count files being watched local watch_count=$(get_watch_files | wc -l) echo_success "========================================== Hot-Recompile Watch Mode Started ========================================== Config: $CONFIG_FILE Mode: ${COMPILE_MODE} (use 'wait' or 'restart' as argument) Monitoring: $watch_count files from config patterns Stable PDF: $STABLE_LINK For rsync: rsync -avL $STABLE_LINK remote:/path/ (The -L flag follows symlinks) Press Ctrl+C to stop ==========================================" # Initial compilation compile_with_lock # Check for inotifywait (preferred) or fall back to polling if command -v inotifywait >/dev/null 2>&1; then echo_info "Using inotify for file watching (efficient)" # Watch for changes using inotify while true; do inotifywait -r -q -e modify,create,delete,move \ "$PROJECT_ROOT/01_manuscript/contents/" \ --exclude '(~$|\.swp$|\.tmp$|#.*#$|\.git)' \ 2>/dev/null # Small delay to batch rapid changes sleep 0.5 # Compile if not locked compile_with_lock echo_info "$(date '+%H:%M:%S') - Waiting for changes..." done else echo_warning "inotifywait not found, using polling mode (less efficient)" echo_info "Install inotify-tools for better performance" # Polling fallback declare -A file_times # Initialize file timestamps while IFS= read -r file; do if [ -f "$file" ]; then file_times["$file"]=$(stat -c %Y "$file" 2>/dev/null) fi done < <(get_watch_files) # Poll for changes while true; do changed=false while IFS= read -r file; do if [ -f "$file" ]; then current_time=$(stat -c %Y "$file" 2>/dev/null) if [ "${file_times[$file]}" != "$current_time" ]; then echo_info "Change detected: $file" file_times["$file"]=$current_time changed=true fi fi done < <(get_watch_files) if [ "$changed" = true ]; then compile_with_lock echo_info "$(date '+%H:%M:%S') - Waiting for changes..." fi sleep 2 # Poll interval done fi } # Check if another watch instance is running if [ -f "$WATCH_PID_FILE" ]; then old_pid=$(cat "$WATCH_PID_FILE" 2>/dev/null) if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then echo_error "Another watch instance is already running (PID: $old_pid)" echo_info "Stop it first with: kill $old_pid" exit 1 else rm -f "$WATCH_PID_FILE" fi fi # Start watching main "$@" # EOF