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:
16
README.md
16
README.md
@@ -488,6 +488,7 @@ Server-Sent Events endpoint for live UI updates.
|
|||||||
- [x] Dark / light theme toggle defaulting to OS colour-scheme preference.
|
- [x] Dark / light theme toggle defaulting to OS colour-scheme preference.
|
||||||
- [x] `default_wait_seconds` changed to fully server-controlled (agents can no longer override wait time).
|
- [x] `default_wait_seconds` changed to fully server-controlled (agents can no longer override wait time).
|
||||||
- [x] Non-blocking `server.ps1` management script (start / stop / restart / status / logs).
|
- [x] Non-blocking `server.ps1` management script (start / stop / restart / status / logs).
|
||||||
|
- [x] Non-blocking `server.sh` bash management script — identical feature set for macOS / Linux.
|
||||||
- [x] MCP stateless/stateful mode configurable via `MCP_STATELESS` env var (default `true`).
|
- [x] MCP stateless/stateful mode configurable via `MCP_STATELESS` env var (default `true`).
|
||||||
- [x] Per-agent generation counter prevents abandoned (timed-out) coroutines from silently consuming instructions meant for newer calls.
|
- [x] Per-agent generation counter prevents abandoned (timed-out) coroutines from silently consuming instructions meant for newer calls.
|
||||||
- [x] `tests/test_wakeup.py` covers both immediate-wakeup timing and concurrent-call generation safety.
|
- [x] `tests/test_wakeup.py` covers both immediate-wakeup timing and concurrent-call generation safety.
|
||||||
@@ -518,8 +519,9 @@ pip install -r requirements.txt
|
|||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use the included management script (recommended — non-blocking):
|
Or use the included management scripts (recommended — non-blocking):
|
||||||
|
|
||||||
|
**PowerShell (Windows)**
|
||||||
```powershell
|
```powershell
|
||||||
.\server.ps1 start # start in background, logs to logs/
|
.\server.ps1 start # start in background, logs to logs/
|
||||||
.\server.ps1 stop # graceful stop
|
.\server.ps1 stop # graceful stop
|
||||||
@@ -530,6 +532,18 @@ Or use the included management script (recommended — non-blocking):
|
|||||||
.\server.ps1 logs 100 # show last 100 lines
|
.\server.ps1 logs 100 # show last 100 lines
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Bash (macOS / Linux)**
|
||||||
|
```bash
|
||||||
|
chmod +x server.sh # make executable once
|
||||||
|
./server.sh start # start in background, logs to logs/
|
||||||
|
./server.sh stop # graceful stop
|
||||||
|
./server.sh restart # stop + start
|
||||||
|
./server.sh status # PID, memory, tail logs
|
||||||
|
./server.sh logs # show last 40 stdout lines
|
||||||
|
./server.sh logs -f # follow logs live
|
||||||
|
./server.sh logs 100 # show last 100 lines
|
||||||
|
```
|
||||||
|
|
||||||
The server starts on `http://localhost:8000` by default.
|
The server starts on `http://localhost:8000` by default.
|
||||||
|
|
||||||
| URL | Description |
|
| URL | Description |
|
||||||
|
|||||||
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