feat: add server.sh bash management script (macOS/Linux)
Equivalent to server.ps1 for bash environments: - start / stop / restart / status / logs [N|-f] - Detached background process via nohup - PID file in logs/ with live-process verification - Port-based fallback detection via lsof - Memory reporting (Linux /proc + macOS ps) - Colour output with ANSI codes - stderr error highlighting in status view - Follow mode (-f) for live log tailing
This commit is contained in:
250
server.sh
Normal file
250
server.sh
Normal file
@@ -0,0 +1,250 @@
|
||||
#!/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
|
||||
|
||||
Reference in New Issue
Block a user