Files
local-mcp/server.sh
Brandon Zhang b1fdd98740 fix(script): add Windows .venv/Scripts path fallback in server.sh
On Windows the venv Python binary lives at .venv/Scripts/python.exe,
not .venv/bin/python.  Fall back to the Windows path when the Unix
path does not exist so the script works cross-platform.
2026-03-27 13:53:38 +08:00

254 lines
7.2 KiB
Bash

#!/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"
if [ ! -f "$PYTHON" ]; then
PYTHON="$ROOT/.venv/Scripts/python.exe"
fi
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