#!/usr/bin/env bash # server.sh — Non-blocking server management script for local-mcp. # # Usage: # ./server.sh start # Start (no-op if already running) # ./server.sh stop # Kill the running server # ./server.sh restart # Stop then start # ./server.sh status # Show PID, memory, and tail 20 log lines # ./server.sh logs [N] # Tail last N lines of stdout log (default 40) # ./server.sh logs -f # Follow log live (Ctrl-C to quit) set -euo pipefail # ── Paths ───────────────────────────────────────────────────────────────── ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PYTHON="$ROOT/.venv/bin/python" ENTRY="$ROOT/main.py" LOG_DIR="$ROOT/logs" LOG_OUT="$LOG_DIR/server.log" LOG_ERR="$LOG_DIR/server.err.log" PID_FILE="$LOG_DIR/server.pid" PORT=8000 # ── Colours ─────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' GRAY='\033[0;90m' NC='\033[0m' # No Colour cecho() { printf "${1}${2}${NC}\n"; } # ── Helpers ─────────────────────────────────────────────────────────────── ensure_log_dir() { mkdir -p "$LOG_DIR" } get_server_pid() { # Trust the PID file first; verify the process is alive if [[ -f "$PID_FILE" ]]; then local stored stored=$(cat "$PID_FILE" 2>/dev/null || true) if [[ "$stored" =~ ^[0-9]+$ ]]; then if kill -0 "$stored" 2>/dev/null; then echo "$stored" return fi fi rm -f "$PID_FILE" fi # Fallback: find a process listening on the port local pid pid=$(lsof -ti "TCP:$PORT" -s "TCP:LISTEN" 2>/dev/null | head -1 || true) if [[ -n "$pid" ]]; then echo "$pid" return fi echo "" } is_running() { local pid pid=$(get_server_pid) [[ -n "$pid" ]] } # ── Commands ────────────────────────────────────────────────────────────── cmd_start() { local pid pid=$(get_server_pid) if [[ -n "$pid" ]]; then cecho "$GREEN" "Server already running (PID $pid http://localhost:$PORT)" return fi if [[ ! -x "$PYTHON" ]]; then cecho "$RED" "ERROR: Python venv not found at $PYTHON" cecho "$YELLOW" "Run: python -m venv .venv && .venv/bin/pip install -r requirements.txt" exit 1 fi ensure_log_dir # Stamp the log files local stamp stamp="$(date '+%Y-%m-%d %H:%M:%S') [server.sh] ---- starting ----" echo "$stamp" >> "$LOG_OUT" echo "$stamp" >> "$LOG_ERR" # Start detached: stdout → LOG_OUT, stderr → LOG_ERR nohup "$PYTHON" -u "$ENTRY" >> "$LOG_OUT" 2>> "$LOG_ERR" & local new_pid=$! echo "$new_pid" > "$PID_FILE" # Wait up to 6 s for the port to open local deadline=$(( $(date +%s) + 6 )) local ready=false while [[ $(date +%s) -lt $deadline ]]; do sleep 0.4 if lsof -ti "TCP:$PORT" -s "TCP:LISTEN" &>/dev/null; then ready=true break fi done if $ready; then cecho "$GREEN" "Server started (PID $new_pid http://localhost:$PORT)" else cecho "$YELLOW" "Server process launched (PID $new_pid) but port $PORT not yet open." cecho "$YELLOW" "Check logs: ./server.sh logs" fi } cmd_stop() { local pid pid=$(get_server_pid) if [[ -z "$pid" ]]; then cecho "$YELLOW" "Server is not running." return fi kill "$pid" 2>/dev/null || true rm -f "$PID_FILE" # Wait up to 4 s for the port to free local deadline=$(( $(date +%s) + 4 )) while [[ $(date +%s) -lt $deadline ]]; do sleep 0.3 if ! lsof -ti "TCP:$PORT" -s "TCP:LISTEN" &>/dev/null; then break fi done cecho "$YELLOW" "Server stopped (was PID $pid)" } cmd_restart() { cmd_stop sleep 0.5 cmd_start } cmd_status() { local pid pid=$(get_server_pid) echo "" if [[ -n "$pid" ]]; then local mem="?" # Try to get RSS memory in MB (Linux: /proc, macOS: ps) if [[ -f "/proc/$pid/status" ]]; then local kb kb=$(grep -i VmRSS "/proc/$pid/status" 2>/dev/null | awk '{print $2}' || echo "0") mem="$(( kb / 1024 )) MB" elif command -v ps &>/dev/null; then local rss rss=$(ps -o rss= -p "$pid" 2>/dev/null | tr -d ' ' || echo "0") mem="$(( rss / 1024 )) MB" fi cecho "$GREEN" " Status : RUNNING" printf " PID : %s\n" "$pid" printf " Memory : %s\n" "$mem" printf " URL : http://localhost:%s\n" "$PORT" printf " Logs : %s\n" "$LOG_OUT" printf " %s\n" "$LOG_ERR" else cecho "$RED" " Status : STOPPED" fi echo "" cecho "$GRAY" "--- Last 20 log lines (stdout) ---" if [[ -f "$LOG_OUT" ]]; then tail -n 20 "$LOG_OUT" | while IFS= read -r line; do cecho "$GRAY" "$line" done else cecho "$GRAY" " (no log file yet)" fi if [[ -f "$LOG_ERR" ]]; then local err_lines err_lines=$(grep -E "ERROR|Exception|Traceback" "$LOG_ERR" 2>/dev/null | tail -5 || true) if [[ -n "$err_lines" ]]; then echo "" cecho "$RED" "--- Recent errors (stderr) ---" echo "$err_lines" | while IFS= read -r line; do cecho "$RED" "$line" done fi fi echo "" } cmd_logs() { local arg="${1:-}" ensure_log_dir if [[ "$arg" == "-f" ]]; then if [[ ! -f "$LOG_OUT" ]]; then cecho "$YELLOW" "No log file yet. Start the server first." return fi cecho "$CYAN" "Following $LOG_OUT (Ctrl-C to stop)" tail -f -n 30 "$LOG_OUT" else local n=40 if [[ "$arg" =~ ^[0-9]+$ ]]; then n="$arg" fi cecho "$CYAN" "--- stdout (last $n lines) ---" if [[ -f "$LOG_OUT" ]]; then tail -n "$n" "$LOG_OUT" else cecho "$GRAY" " (empty)" fi echo "" cecho "$YELLOW" "--- stderr (last 20 lines) ---" if [[ -f "$LOG_ERR" ]]; then tail -n 20 "$LOG_ERR" else cecho "$GRAY" " (empty)" fi fi } # ── Dispatch ────────────────────────────────────────────────────────────── COMMAND="${1:-status}" ARG="${2:-}" case "$COMMAND" in start) cmd_start ;; stop) cmd_stop ;; restart) cmd_restart ;; status) cmd_status ;; logs) cmd_logs "$ARG" ;; *) echo "Usage: $0 {start|stop|restart|status|logs [N|-f]}" exit 1 ;; esac