Files
unraid-mcp/dev.sh
Jacob Magar 2df6115183 Move pid and log files to /tmp directory
- Update dev.sh to use /tmp for LOG_DIR instead of PROJECT_DIR/logs
- Update settings.py to use /tmp for LOGS_DIR instead of PROJECT_ROOT/logs
- This change moves both pid files and log files to the temporary directory

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-15 00:24:26 -04:00

556 lines
17 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Unraid MCP Server Development Script
# Safely manages server processes during development with accurate process detection
set -euo pipefail
# Configuration
DEFAULT_PORT=6970
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="/tmp"
LOG_FILE="$LOG_DIR/unraid-mcp.log"
PID_FILE="$LOG_DIR/dev.pid"
# Ensure logs directory exists
mkdir -p "$LOG_DIR"
# All colors are now handled by Rich logging system
# Helper function for unified Rich logging
log() {
local message="$1"
local level="${2:-info}"
local indent="${3:-0}"
local file_timestamp="$(date +'%Y-%m-%d %H:%M:%S')"
# Use unified Rich logger for beautiful console output - escape single quotes
local escaped_message="${message//\'/\'\"\'\"\'}"
uv run python -c "from unraid_mcp.config.logging import log_with_level_and_indent; log_with_level_and_indent('$escaped_message', '$level', $indent)"
# File output without color
printf "[%s] %s\n" "$file_timestamp" "$message" >> "$LOG_FILE"
}
# Convenience functions for different log levels
log_error() { log "$1" "error" "${2:-0}"; }
log_warning() { log "$1" "warning" "${2:-0}"; }
log_success() { log "$1" "success" "${2:-0}"; }
log_info() { log "$1" "info" "${2:-0}"; }
log_status() { log "$1" "status" "${2:-0}"; }
# Rich header function
log_header() {
uv run python -c "from unraid_mcp.config.logging import log_header; log_header('$1')"
}
# Rich separator function
log_separator() {
uv run python -c "from unraid_mcp.config.logging import log_separator; log_separator()"
}
# Get port from environment or use default
get_port() {
local port="${UNRAID_MCP_PORT:-$DEFAULT_PORT}"
echo "$port"
}
# Write PID to file
write_pid_file() {
local pid=$1
echo "$pid" > "$PID_FILE"
}
# Read PID from file
read_pid_file() {
if [[ -f "$PID_FILE" ]]; then
cat "$PID_FILE" 2>/dev/null
fi
}
# Check if PID file contains valid running process
is_pid_valid() {
local pid=$1
[[ -n "$pid" ]] && [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null
}
# Clean up PID file
cleanup_pid_file() {
if [[ -f "$PID_FILE" ]]; then
rm -f "$PID_FILE"
log_info "🗑️ Cleaned up PID file"
fi
}
# Get PID from PID file if valid, otherwise return empty
get_valid_pid_from_file() {
local pid=$(read_pid_file)
if is_pid_valid "$pid"; then
echo "$pid"
else
# Clean up stale PID file
[[ -f "$PID_FILE" ]] && cleanup_pid_file
echo ""
fi
}
# Find processes using multiple detection methods
find_server_processes() {
local port=$(get_port)
local pids=()
# Method 0: Check PID file first (most reliable)
local pid_from_file=$(get_valid_pid_from_file)
if [[ -n "$pid_from_file" ]]; then
log_status "🔍 Found server PID from file: $pid_from_file"
pids+=("$pid_from_file")
fi
# Method 1: Command line pattern matching (fallback)
while IFS= read -r line; do
if [[ -n "$line" ]]; then
local pid=$(echo "$line" | awk '{print $2}')
# Add to pids if not already present
if [[ ! " ${pids[@]} " =~ " $pid " ]]; then
pids+=("$pid")
fi
fi
done < <(ps aux | grep -E 'python.*unraid.*mcp|python.*main\.py|uv run.*main\.py|uv run -m unraid_mcp' | grep -v grep | grep -v "$0")
# Method 2: Port binding verification (fallback)
if command -v lsof >/dev/null 2>&1; then
while IFS= read -r line; do
if [[ -n "$line" ]]; then
local pid=$(echo "$line" | awk '{print $2}')
# Add to pids if not already present
if [[ ! " ${pids[@]} " =~ " $pid " ]]; then
pids+=("$pid")
fi
fi
done < <(lsof -i ":$port" 2>/dev/null | grep LISTEN || true)
fi
# Method 3: Working directory verification for fallback methods
local verified_pids=()
for pid in "${pids[@]}"; do
# Skip if not a valid PID
if ! [[ "$pid" =~ ^[0-9]+$ ]]; then
continue
fi
# If this PID came from the PID file, it's already verified
if [[ "$pid" == "$pid_from_file" ]]; then
verified_pids+=("$pid")
continue
fi
# Verify other PIDs by working directory
if [[ -d "/proc/$pid" ]]; then
local pwd_info=""
if command -v pwdx >/dev/null 2>&1; then
pwd_info=$(pwdx "$pid" 2>/dev/null | cut -d' ' -f2- || echo "unknown")
else
pwd_info=$(readlink -f "/proc/$pid/cwd" 2>/dev/null || echo "unknown")
fi
# Verify it's running from our project directory or a parent directory
if [[ "$pwd_info" == "$PROJECT_DIR"* ]] || [[ "$pwd_info" == *"unraid-mcp"* ]]; then
verified_pids+=("$pid")
fi
fi
done
# Output final list
printf '%s\n' "${verified_pids[@]}" | grep -E '^[0-9]+$' || true
}
# Terminate a process gracefully, then forcefully if needed
terminate_process() {
local pid=$1
local name=${2:-"process"}
if ! kill -0 "$pid" 2>/dev/null; then
log_warning "⚠️ Process $pid ($name) already terminated"
return 0
fi
log_warning "🔄 Terminating $name (PID: $pid)..."
# Step 1: Graceful shutdown (SIGTERM)
log_info "→ Sending SIGTERM to PID $pid" 1
kill -TERM "$pid" 2>/dev/null || {
log_warning "⚠️ Failed to send SIGTERM (process may have died)" 2
return 0
}
# Step 2: Wait for graceful shutdown (5 seconds)
local count=0
while [[ $count -lt 5 ]]; do
if ! kill -0 "$pid" 2>/dev/null; then
log_success "✅ Process $pid terminated gracefully" 1
# Clean up PID file if this was our server process
local pid_from_file=$(read_pid_file)
if [[ "$pid" == "$pid_from_file" ]]; then
cleanup_pid_file
fi
return 0
fi
sleep 1
((count++))
log_info "⏳ Waiting for graceful shutdown... (${count}/5)" 2
done
# Step 3: Force kill (SIGKILL)
log_error "⚡ Graceful shutdown timeout, sending SIGKILL to PID $pid" 1
kill -KILL "$pid" 2>/dev/null || {
log_warning "⚠️ Failed to send SIGKILL (process may have died)" 2
return 0
}
# Step 4: Final verification
sleep 1
if kill -0 "$pid" 2>/dev/null; then
log_error "❌ Failed to terminate process $pid" 1
return 1
else
log_success "✅ Process $pid terminated forcefully" 1
# Clean up PID file if this was our server process
local pid_from_file=$(read_pid_file)
if [[ "$pid" == "$pid_from_file" ]]; then
cleanup_pid_file
fi
return 0
fi
}
# Stop all server processes
stop_servers() {
log_header "Server Shutdown"
log_error "🛑 Stopping existing server processes..."
local pids=($(find_server_processes))
if [[ ${#pids[@]} -eq 0 ]]; then
log_success "✅ No processes to stop"
return 0
fi
local failed=0
for pid in "${pids[@]}"; do
if ! terminate_process "$pid" "Unraid MCP Server"; then
((failed++))
fi
done
# Wait for ports to be released
local port=$(get_port)
log_info "⏳ Waiting for port $port to be released..."
local port_wait=0
while [[ $port_wait -lt 3 ]]; do
if ! lsof -i ":$port" >/dev/null 2>&1; then
log_success "✅ Port $port released" 1
break
fi
sleep 1
((port_wait++))
done
if [[ $failed -gt 0 ]]; then
log_error "⚠️ Failed to stop $failed process(es)"
return 1
else
log_success "✅ All processes stopped successfully"
return 0
fi
}
# Start the new modular server
start_modular_server() {
log_header "Modular Server Startup"
log_success "🚀 Starting modular server..."
cd "$PROJECT_DIR"
# Check if main.py exists in unraid_mcp/
if [[ ! -f "unraid_mcp/main.py" ]]; then
log_error "❌ unraid_mcp/main.py not found. Make sure modular server is implemented."
return 1
fi
# Clear the log file and add a startup marker to capture fresh logs
echo "=== Server Starting at $(date) ===" > "$LOG_FILE"
# Start server in background using module syntax
log_info "→ Executing: uv run -m unraid_mcp.main" 1
# Start server in new process group to isolate it from parent signals
setsid nohup uv run -m unraid_mcp.main >> "$LOG_FILE" 2>&1 &
local pid=$!
# Write PID to file
write_pid_file "$pid"
log_info "📝 Written PID $pid to file: $PID_FILE" 1
# Give it a moment to start and write some logs
sleep 3
# Check if it's still running
if kill -0 "$pid" 2>/dev/null; then
local port=$(get_port)
log_success "✅ Modular server started successfully (PID: $pid, Port: $port)"
log_info "📋 Process info: $(ps -p "$pid" -o pid,ppid,cmd --no-headers 2>/dev/null || echo 'Process info unavailable')" 1
# Auto-tail logs after successful start
echo ""
log_success "📄 Following server logs in real-time..."
log_info " Press Ctrl+C to stop following logs (server will continue running)" 1
log_separator
echo ""
# Set up signal handler for graceful exit from log following
trap 'handle_log_interrupt' SIGINT
# Start tailing from beginning of the fresh log file
tail -f "$LOG_FILE"
return 0
else
log_error "❌ Modular server failed to start"
cleanup_pid_file
log_warning "📄 Check $LOG_FILE for error details"
return 1
fi
}
# Start the original server
start_original_server() {
log_header "Original Server Startup"
log_success "🚀 Starting original server..."
cd "$PROJECT_DIR"
# Check if original server exists
if [[ ! -f "unraid_mcp_server.py" ]]; then
log_error "❌ unraid_mcp_server.py not found"
return 1
fi
# Clear the log file and add a startup marker to capture fresh logs
echo "=== Server Starting at $(date) ===" > "$LOG_FILE"
# Start server in background
log_info "→ Executing: uv run unraid_mcp_server.py" 1
# Start server in new process group to isolate it from parent signals
setsid nohup uv run unraid_mcp_server.py >> "$LOG_FILE" 2>&1 &
local pid=$!
# Write PID to file
write_pid_file "$pid"
log_info "📝 Written PID $pid to file: $PID_FILE" 1
# Give it a moment to start and write some logs
sleep 3
# Check if it's still running
if kill -0 "$pid" 2>/dev/null; then
local port=$(get_port)
log_success "✅ Original server started successfully (PID: $pid, Port: $port)"
log_info "📋 Process info: $(ps -p "$pid" -o pid,ppid,cmd --no-headers 2>/dev/null || echo 'Process info unavailable')" 1
# Auto-tail logs after successful start
echo ""
log_success "📄 Following server logs in real-time..."
log_info " Press Ctrl+C to stop following logs (server will continue running)" 1
log_separator
echo ""
# Set up signal handler for graceful exit from log following
trap 'handle_log_interrupt' SIGINT
# Start tailing from beginning of the fresh log file
tail -f "$LOG_FILE"
return 0
else
log_error "❌ Original server failed to start"
cleanup_pid_file
log_warning "📄 Check $LOG_FILE for error details"
return 1
fi
}
# Show usage information
show_usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Development script for Unraid MCP Server"
echo ""
echo "OPTIONS:"
echo " (no args) Stop existing servers, start modular server, and tail logs"
echo " --old Stop existing servers, start original server, and tail logs"
echo " --kill Stop existing servers only (don't start new one)"
echo " --status Show status of running servers"
echo " --logs [N] Show last N lines of server logs (default: 50)"
echo " --tail Follow server logs in real-time (without restarting server)"
echo " --help, -h Show this help message"
echo ""
echo "ENVIRONMENT VARIABLES:"
echo " UNRAID_MCP_PORT Port for server (default: $DEFAULT_PORT)"
echo ""
echo "EXAMPLES:"
echo " ./dev.sh # Restart with modular server"
echo " ./dev.sh --old # Restart with original server"
echo " ./dev.sh --kill # Stop all servers"
echo " ./dev.sh --status # Check server status"
echo " ./dev.sh --logs # Show last 50 lines of logs"
echo " ./dev.sh --logs 100 # Show last 100 lines of logs"
echo " ./dev.sh --tail # Follow logs in real-time"
}
# Show server status
show_status() {
local port=$(get_port)
log_header "Server Status"
log_status "🔍 Server Status Check"
log_info "📁 Project Directory: $PROJECT_DIR" 1
log_info "📝 PID File: $PID_FILE" 1
log_info "🔌 Expected Port: $port" 1
echo ""
# Check PID file status
local pid_from_file=$(read_pid_file)
if [[ -n "$pid_from_file" ]]; then
if is_pid_valid "$pid_from_file"; then
log_success "✅ PID File: Contains valid PID $pid_from_file" 1
else
log_warning "⚠️ PID File: Contains stale PID $pid_from_file (process not running)" 1
fi
else
log_warning "🚫 PID File: Not found or empty" 1
fi
echo ""
local pids=($(find_server_processes))
if [[ ${#pids[@]} -eq 0 ]]; then
log_warning "🟡 Status: No servers running" 1
else
log_success "✅ Status: ${#pids[@]} server(s) running" 1
for pid in "${pids[@]}"; do
local cmd=$(ps -p "$pid" -o cmd --no-headers 2>/dev/null || echo "Command unavailable")
local source="process scan"
if [[ "$pid" == "$pid_from_file" ]]; then
source="PID file"
fi
log_success "PID $pid ($source): $cmd" 2
done
fi
# Check port binding
if command -v lsof >/dev/null 2>&1; then
local port_info=$(lsof -i ":$port" 2>/dev/null | grep LISTEN || echo "")
if [[ -n "$port_info" ]]; then
log_success "Port $port: BOUND" 1
echo "$port_info" | while IFS= read -r line; do
log_info "$line" 2
done
else
log_warning "Port $port: FREE" 1
fi
fi
}
# Tail the server logs
tail_logs() {
local lines="${1:-50}"
log_info "📄 Tailing last $lines lines from server logs..."
if [[ ! -f "$LOG_FILE" ]]; then
log_error "❌ Log file not found: $LOG_FILE"
return 1
fi
echo ""
echo "=== Server Logs (last $lines lines) ==="
tail -n "$lines" "$LOG_FILE"
echo "=== End of Logs ===="
echo ""
}
# Handle SIGINT during log following
handle_log_interrupt() {
echo ""
log_info "📄 Stopped following logs. Server continues running in background."
log_info "💡 Use './dev.sh --status' to check server status" 1
log_info "💡 Use './dev.sh --tail' to resume following logs" 1
exit 0
}
# Follow server logs in real-time
follow_logs() {
log_success "📄 Following server logs in real-time..."
log_info " Press Ctrl+C to stop following logs"
if [[ ! -f "$LOG_FILE" ]]; then
log_error "❌ Log file not found: $LOG_FILE"
return 1
fi
# Set up signal handler for graceful exit
trap 'handle_log_interrupt' SIGINT
log_separator
echo ""
tail -f "$LOG_FILE"
}
# Main script logic
main() {
# Initialize log file
echo "=== Dev Script Started at $(date) ===" >> "$LOG_FILE"
case "${1:-}" in
--help|-h)
show_usage
;;
--status)
show_status
;;
--kill)
stop_servers
;;
--logs)
tail_logs "${2:-50}"
;;
--tail)
follow_logs
;;
--old)
if stop_servers; then
start_original_server
else
log_error "❌ Failed to stop existing servers"
exit 1
fi
;;
"")
if stop_servers; then
start_modular_server
else
log_error "❌ Failed to stop existing servers"
exit 1
fi
;;
*)
log_error "❌ Unknown option: $1"
show_usage
exit 1
;;
esac
}
# Run main function with all arguments
main "$@"