Files
local-mcp/server.ps1
2026-03-27 03:58:57 +08:00

230 lines
7.7 KiB
PowerShell

<#
.SYNOPSIS
Non-blocking server management script for local-mcp.
.DESCRIPTION
Manages the local-mcp server process without blocking the terminal.
The server runs as a detached background process; stdout/stderr are
written to logs/server.log.
.USAGE
.\server.ps1 start # Start (no-op if already running)
.\server.ps1 stop # Kill the running server
.\server.ps1 restart # Stop then start
.\server.ps1 status # Show PID, port state, and tail 20 log lines
.\server.ps1 logs [N] # Tail last N lines of the log (default 40)
.\server.ps1 logs -f # Follow log live (Ctrl-C to quit)
#>
param(
[Parameter(Position = 0)]
[ValidateSet("start", "stop", "restart", "status", "logs")]
[string]$Command = "status",
[Parameter(Position = 1)]
[string]$Arg = ""
)
# ── Paths ─────────────────────────────────────────────────────────────────
$Root = $PSScriptRoot
$Python = Join-Path $Root ".venv\Scripts\python.exe"
$Entry = Join-Path $Root "main.py"
$LogDir = Join-Path $Root "logs"
$LogOut = Join-Path $LogDir "server.log"
$LogErr = Join-Path $LogDir "server.err.log"
$PidFile = Join-Path $LogDir "server.pid"
$Port = 8000
# ── Helpers ───────────────────────────────────────────────────────────────
function EnsureLogDir {
if (-not (Test-Path $LogDir)) {
New-Item -ItemType Directory -Path $LogDir | Out-Null
}
}
function GetServerPid {
# Trust the PID file first; verify the process is actually alive
if (Test-Path $PidFile) {
$stored = Get-Content $PidFile -ErrorAction SilentlyContinue
if ($stored -match '^\d+$') {
$proc = Get-Process -Id ([int]$stored) -ErrorAction SilentlyContinue
if ($proc) { return [int]$stored }
}
Remove-Item $PidFile -ErrorAction SilentlyContinue
}
# Fallback: find python process listening on the port
$conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($conn) { return $conn.OwningProcess }
return $null
}
function IsRunning {
return $null -ne (GetServerPid)
}
function WriteLog([string]$msg, [string]$color = "White") {
Write-Host $msg -ForegroundColor $color
}
function MergedLogTail([int]$n) {
# Merge stdout + stderr logs sorted by content order and tail n lines
$lines = @()
if (Test-Path $LogOut) { $lines += Get-Content $LogOut -Tail ($n * 2) }
if (Test-Path $LogErr) { $lines += Get-Content $LogErr -Tail ($n * 2) }
# Return last $n lines (simple approach — interleaving is approximate)
$lines | Select-Object -Last $n
}
# ── Commands ──────────────────────────────────────────────────────────────
function Start-Server {
if (IsRunning) {
$pid_ = GetServerPid
WriteLog "Server already running (PID $pid_ http://localhost:$Port)" "Green"
return
}
if (-not (Test-Path $Python)) {
WriteLog "ERROR: Python venv not found at $Python" "Red"
WriteLog "Run: python -m venv .venv && .venv\Scripts\pip install -r requirements.txt" "Yellow"
exit 1
}
EnsureLogDir
# Stamp both log files so they exist and have a separator
$stamp = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [server.ps1] ---- starting ----"
Add-Content $LogOut $stamp
Add-Content $LogErr $stamp
# Start python with stdout -> LogOut, stderr -> LogErr (separate files required on Windows)
$proc = Start-Process `
-FilePath $Python `
-ArgumentList "-u `"$Entry`"" `
-WorkingDirectory $Root `
-RedirectStandardOutput $LogOut `
-RedirectStandardError $LogErr `
-WindowStyle Hidden `
-PassThru
$proc.Id | Set-Content $PidFile
# Wait up to 6 s for the port to open
$deadline = (Get-Date).AddSeconds(6)
$ready = $false
while ((Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 400
$conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
if ($conn) { $ready = $true; break }
}
if ($ready) {
WriteLog "Server started (PID $($proc.Id) http://localhost:$Port)" "Green"
} else {
WriteLog "Server process launched (PID $($proc.Id)) but port $Port not yet open." "Yellow"
WriteLog "Check logs: .\server.ps1 logs" "Yellow"
}
}
function Stop-Server {
$pid_ = GetServerPid
if (-not $pid_) {
WriteLog "Server is not running." "Yellow"
return
}
Stop-Process -Id $pid_ -Force -ErrorAction SilentlyContinue
Remove-Item $PidFile -ErrorAction SilentlyContinue
# Wait up to 4 s for the port to free
$deadline = (Get-Date).AddSeconds(4)
while ((Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 300
$conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
if (-not $conn) { break }
}
WriteLog "Server stopped (was PID $pid_)" "Yellow"
}
function Restart-Server {
Stop-Server
Start-Sleep -Milliseconds 500
Start-Server
}
function Show-Status {
$pid_ = GetServerPid
if ($pid_) {
$proc = Get-Process -Id $pid_ -ErrorAction SilentlyContinue
$mem = if ($proc) { "{0:N0} MB" -f ($proc.WorkingSet64 / 1MB) } else { "?" }
WriteLog ""
WriteLog " Status : RUNNING" "Green"
WriteLog " PID : $pid_"
WriteLog " Memory : $mem"
WriteLog " URL : http://localhost:$Port"
WriteLog " Logs : $LogOut"
WriteLog " $LogErr"
} else {
WriteLog ""
WriteLog " Status : STOPPED" "Red"
}
WriteLog ""
WriteLog "--- Last 20 log lines (stdout) ---" "DarkGray"
if (Test-Path $LogOut) {
Get-Content $LogOut -Tail 20 | ForEach-Object { WriteLog $_ "DarkGray" }
} else {
WriteLog " (no log file yet)" "DarkGray"
}
if (Test-Path $LogErr) {
$errLines = Get-Content $LogErr -Tail 5 | Where-Object { $_ -match "ERROR|Exception|Traceback" }
if ($errLines) {
WriteLog ""
WriteLog "--- Recent errors (stderr) ---" "Red"
$errLines | ForEach-Object { WriteLog $_ "Red" }
}
}
WriteLog ""
}
function Tail-Logs {
EnsureLogDir
if ($Arg -eq "-f") {
if (-not (Test-Path $LogOut)) {
WriteLog "No log file yet. Start the server first." "Yellow"
return
}
WriteLog "Following $LogOut (Ctrl-C to stop)" "Cyan"
Get-Content $LogOut -Wait -Tail 30
} else {
$n = if ($Arg -match '^\d+$') { [int]$Arg } else { 40 }
WriteLog "--- stdout (last $n lines) ---" "Cyan"
if (Test-Path $LogOut) { Get-Content $LogOut -Tail $n } else { WriteLog " (empty)" "DarkGray" }
WriteLog ""
WriteLog "--- stderr (last 20 lines) ---" "Yellow"
if (Test-Path $LogErr) { Get-Content $LogErr -Tail 20 } else { WriteLog " (empty)" "DarkGray" }
}
}
# ── Dispatch ──────────────────────────────────────────────────────────────
switch ($Command) {
"start" { Start-Server }
"stop" { Stop-Server }
"restart" { Restart-Server }
"status" { Show-Status }
"logs" { Tail-Logs }
}