升级为win服务

This commit is contained in:
2026-04-14 13:16:37 +08:00
parent e6d2cbdefd
commit ed5dfd9452
6 changed files with 723 additions and 326 deletions
-305
View File
@@ -1,305 +0,0 @@
#Requires -Version 5.1
<#
.SYNOPSIS
FastAPI Server Startup Script (PowerShell Version)
.DESCRIPTION
Start FastAPI server with environment variable configuration, port checking and auto cleanup
.PARAMETER Port
Server port number (default: 8000, can be overridden from .env file or command line parameter)
.PARAMETER Host
Server host address (default: 0.0.0.0)
.EXAMPLE
.\run.ps1
.\run.ps1 -Port 8001
.\run.ps1 -Port 8001 -Host 127.0.0.1
#>
[CmdletBinding()]
param(
[Parameter(Position=0)]
[int]$Port = 8000,
[Parameter()]
[string]$HostAddress = "0.0.0.0"
)
# Set output encoding to UTF-8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
# Configuration variables
$App = "main:app"
$LogFile = "logs\fastapi_server.log"
$EnvFile = ".env"
# Function: Write log
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "$timestamp - $Message"
Add-Content -Path $LogFile -Value $logEntry -ErrorAction SilentlyContinue
}
# Function: Load .env file
function Import-DotEnv {
param([string]$Path)
if (-not (Test-Path $Path)) {
return
}
Write-Host "Loading environment variables from .env file..."
Get-Content $Path | ForEach-Object {
$line = $_.Trim()
# Skip empty lines and comments
if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith("#")) {
return
}
# Parse KEY=VALUE
if ($line -match "^([^=]+)=(.*)$") {
$key = $matches[1].Trim()
$value = $matches[2].Trim()
# Remove possible quotes
$value = $value -replace "^[`"']|[`"']$"
# Set environment variable
[Environment]::SetEnvironmentVariable($key, $value, "Process")
}
}
}
# Function: Check if port is in use
function Test-PortInUse {
param([int]$PortNumber)
$connections = Get-NetTCPConnection -LocalPort $PortNumber -ErrorAction SilentlyContinue
return $connections.Count -gt 0
}
# Function: Get process using port
function Get-PortProcess {
param([int]$PortNumber)
$connection = Get-NetTCPConnection -LocalPort $PortNumber -ErrorAction SilentlyContinue | Select-Object -First 1
if ($connection) {
return Get-Process -Id $connection.OwningProcess -ErrorAction SilentlyContinue
}
return $null
}
# Function: Clear port usage
function Clear-Port {
param([int]$PortNumber)
Write-Host "Checking if port $PortNumber is available..."
$process = Get-PortProcess -PortNumber $PortNumber
if ($process) {
# Skip Idle process (PID 0) as it's a system process and cannot be killed
if ($process.Id -eq 0) {
Write-Host "[INFO] Found Idle process (PID: 0), skipping termination as it's a system process" -ForegroundColor Yellow
Write-Log "Found Idle process (PID: 0), skipping termination"
} else {
Write-Host "[WARNING] Found process $($process.ProcessName) (PID: $($process.Id)) using port $PortNumber" -ForegroundColor Yellow
Write-Log "Found process $($process.Id) using port $PortNumber"
Write-Host "Killing process $($process.Id)..."
try {
Stop-Process -Id $process.Id -Force -ErrorAction Stop
Write-Log "Killed process $($process.Id)"
Write-Host "[INFO] Process killed successfully" -ForegroundColor Green
}
catch {
Write-Host "[ERROR] Failed to kill process: $_" -ForegroundColor Red
Write-Log "Failed to kill process $($process.Id): $_"
}
}
}
# Wait for port release
Start-Sleep -Seconds 5
# Verify port is available
if (Test-PortInUse -PortNumber $PortNumber) {
# Check if it's still the Idle process
$process = Get-PortProcess -PortNumber $PortNumber
if ($process -and $process.Id -eq 0) {
# Idle process doesn't actually use the port, so consider it available
Write-Host "[INFO] Port $PortNumber is available for use (Idle process detected)" -ForegroundColor Green
Write-Log "Port $PortNumber is available (Idle process detected)"
return $true
} else {
Write-Host "[ERROR] Port $PortNumber is still in use!" -ForegroundColor Red
Write-Log "Port $PortNumber is still in use!"
return $false
}
}
else {
Write-Host "[INFO] Port $PortNumber is available for use" -ForegroundColor Green
Write-Log "Port $PortNumber is available"
return $true
}
}
# Function: Find Python interpreter
function Find-Python {
$venvPython = "venv\Scripts\python.exe"
if (Test-Path $venvPython) {
Write-Host "Using virtual Python"
return (Resolve-Path $venvPython).Path
}
else {
Write-Host "Using system Python"
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
if ($pythonCmd) {
return $pythonCmd.Source
}
throw "Python not found!"
}
}
# ==================== Main Program ====================
try {
# Create log directory
if (-not (Test-Path "logs")) {
New-Item -ItemType Directory -Path "logs" -Force | Out-Null
}
Write-Host "[INFO] Logs directory ensured"
# Load configuration from .env file (command line parameter has highest priority)
Import-DotEnv -Path $EnvFile
# Read default values from environment variables (if not specified in command line)
if ($PSBoundParameters.ContainsKey("Port") -eq $false -and $env:PORT) {
$Port = [int]$env:PORT
}
if ($PSBoundParameters.ContainsKey("Host") -eq $false -and $env:HOST) {
$HostAddress = $env:HOST
}
# Set environment variables
[Environment]::SetEnvironmentVariable("PORT", $Port, "Process")
[Environment]::SetEnvironmentVariable("HOST", $HostAddress, "Process")
[Environment]::SetEnvironmentVariable("START_MODE", "script", "Process")
[Environment]::SetEnvironmentVariable("PYTHONPATH", (Get-Location).Path, "Process")
# Set unbuffered output for better log display
[Environment]::SetEnvironmentVariable("PYTHONUNBUFFERED", "1", "Process")
# Display configuration information
Write-Host ""
Write-Host "FastAPI Server Starting..." -ForegroundColor Cyan
Write-Host "Port: $Port, Host: $HostAddress"
Write-Host "Press Ctrl+C to stop gracefully"
Write-Host ""
# Record startup log
Write-Log "Server starting on port $Port"
# Clear port usage
if (-not (Clear-Port -PortNumber $Port)) {
exit 1
}
# Find Python
$Python = Find-Python
Write-Host "Python: $Python"
# Start server
Write-Host ""
Write-Host "Starting uvicorn server with host $HostAddress and port $Port..." -ForegroundColor Cyan
Write-Log "Starting uvicorn server with host $HostAddress and port $Port"
# Build uvicorn arguments
$uvicornArgs = @(
"-m", "uvicorn",
$App,
"--host", $HostAddress,
"--port", "$Port",
"--log-level", "info",
"--access-log"
)
# Start process with graceful shutdown support
$serverProcess = Start-Process -FilePath $Python -ArgumentList $uvicornArgs -NoNewWindow -PassThru
# Setup graceful shutdown handler
$shutdownRequested = $false
$null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
$script:shutdownRequested = $true
}
try {
# Wait for process to exit
Wait-Process -Id $serverProcess.Id -ErrorAction Stop
}
catch {
if ($shutdownRequested -or $Error[0].Exception.Message -match "was not found") {
Write-Host ""
Write-Host "Shutting down server gracefully..." -ForegroundColor Yellow
Write-Log "Shutting down server gracefully"
# Try graceful shutdown first
$serverProcess.CloseMainWindow() | Out-Null
Start-Sleep -Seconds 5
# Force kill if still running
if (!$serverProcess.HasExited) {
Write-Host "Force killing server process..." -ForegroundColor Yellow
$serverProcess | Stop-Process -Force -ErrorAction SilentlyContinue
}
}
else {
Write-Host ""
Write-Host "[ERROR] Server process error: $_" -ForegroundColor Red
Write-Log "Server process error: $_"
}
}
# Get actual exit code
$exitCode = $serverProcess.ExitCode
# Check exit code
if ($exitCode -ne 0) {
Write-Host ""
Write-Host "[ERROR] Application failed to start with error code $exitCode" -ForegroundColor Red
Write-Log "Application failed to start with error code $exitCode"
exit $exitCode
}
}
catch {
Write-Host ""
Write-Host "[ERROR] An error occurred: $_" -ForegroundColor Red
Write-Log "Error: $_"
exit 1
}
finally {
# Cleanup work
Write-Host ""
Write-Host "Server shutting down..." -ForegroundColor Yellow
Write-Log "Server shutting down"
# Clear port usage
Write-Host "Cleaning up any remaining processes on port $Port..."
$process = Get-PortProcess -PortNumber $Port
if ($process) {
try {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
Write-Host "Killed process $($process.Id)"
Write-Log "Killed process $($process.Id) during cleanup"
}
catch {
# Ignore errors
}
}
Write-Host "Shutdown complete." -ForegroundColor Green
Write-Log "Server shutdown complete"
Write-Host ""
}
+16 -14
View File
@@ -1,11 +1,14 @@
@echo off
title MyAPS API SERVER
:: 配置变量
set "PS_SCRIPT=%~dp0run.ps1"
set "ENV_FILE=%~dp0.env"
set "VENV_PYTHON=%~dp0..\venv\Scripts\python.exe"
set "PROJECT_ROOT=%~dp0.."
set "ENV_FILE=%PROJECT_ROOT%\.env"
set "RESTART_DELAY=5"
set "MAX_RESTARTS=5"
set "RESTART_COUNT=0"
set "HOST=0.0.0.0"
set "PORT=8001"
:: 从 .env 文件读取 HOST 和 PORT 配置
if exist "%ENV_FILE%" (
@@ -16,33 +19,32 @@ if exist "%ENV_FILE%" (
:: 显示启动信息
echo =========================================
echo FastAPI Server Monitor
echo Monitoring: %PS_SCRIPT%
echo Project Root: %PROJECT_ROOT%
echo Python: %VENV_PYTHON%
echo Environment: %ENV_FILE%
echo Host: %HOST%
echo Port: %PORT%
echo Press Ctrl+C twice to stop monitoring
echo Press Ctrl+C to stop
echo =========================================
echo.
:: 进入项目根目录
cd /d "%PROJECT_ROOT%"
:: 无限循环监测
:LOOP
echo [%date% %time%] Starting run.ps1...
echo [%date% %time%] Starting FastAPI server...
:: 构建命令参数
set "PS_ARGS="
if defined HOST set "PS_ARGS=%PS_ARGS% -HostAddress %HOST%"
if defined PORT set "PS_ARGS=%PS_ARGS% -Port %PORT%"
:: 执行 PowerShell 脚本
powershell -ExecutionPolicy Bypass -NoProfile -Command "& '%PS_SCRIPT%' %PS_ARGS% %*"
:: 执行 Python 命令
%VENV_PYTHON% -m uvicorn main:app --host %HOST% --port %PORT% --log-level info --access-log
:: 检查退出码(0=正常退出,非0=异常退出)
if %errorlevel% equ 0 (
echo [%date% %time%] run.ps1 exited normally.
echo [%date% %time%] Server exited normally.
goto END
) else (
set /a "RESTART_COUNT=%RESTART_COUNT%+1"
echo [%date% %time%] run.ps1 exited with error code %errorlevel%.
echo [%date% %time%] Server exited with error code %errorlevel%.
echo [%date% %time%] Restart count: %RESTART_COUNT%/%MAX_RESTARTS%
:: 检查是否达到最大重启次数
BIN
View File
Binary file not shown.
+492
View File
@@ -0,0 +1,492 @@
#Requires -Version 5.1
<#
.SYNOPSIS
FastAPI Server Startup Script
.DESCRIPTION
Start FastAPI server with support for both service mode and development mode
.PARAMETER Port
Server port number (default: 8000, can be overridden from .env file or command line parameter)
.PARAMETER Host
Server host address (default: 0.0.0.0)
.PARAMETER Mode
Run mode: "service" for Windows service, "dev" for development (default: "dev")
.EXAMPLE
.\run.ps1
.\run.ps1 -Port 8001
.\run.ps1 -Mode service
#>
[CmdletBinding()]
param(
[Parameter(Position=0)]
[int]$Port = 8000,
[Parameter()]
[string]$HostAddress = "0.0.0.0",
[Parameter()]
[ValidateSet("service", "dev")]
[string]$Mode = "dev"
)
# Set output encoding to UTF-8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
# Get project root directory
$ProjectRoot = Split-Path -Parent $PSScriptRoot
# Set working directory to project root
Set-Location -Path $ProjectRoot
if ($Mode -eq "dev") {
Write-Host "Working directory set to: $ProjectRoot"
}
# Configuration variables
$App = "main:app"
$LogFile = "$ProjectRoot\logs\fastapi_server.log"
$EnvFile = "$ProjectRoot\.env"
# Function: Write log
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "$timestamp - $Message"
try {
if (-not (Test-Path "$ProjectRoot\logs")) {
New-Item -ItemType Directory -Path "$ProjectRoot\logs" -Force | Out-Null
}
Add-Content -Path $LogFile -Value $logEntry
} catch {
# Ignore logging errors
}
}
# Function: Load .env file
function Import-DotEnv {
param([string]$Path)
if (-not (Test-Path $Path)) {
Write-Log "WARNING: .env file not found at $Path"
if ($Mode -eq "dev") {
Write-Host "WARNING: .env file not found at $Path"
}
return
}
Write-Log "Loading environment variables from .env file..."
if ($Mode -eq "dev") {
Write-Host "Loading environment variables from .env file..."
}
Get-Content $Path | ForEach-Object {
$line = $_.Trim()
# Skip empty lines and comments
if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith("#")) {
return
}
# Parse KEY=VALUE
if ($line -match "^([^=]+)=(.*)$") {
$key = $matches[1].Trim()
$value = $matches[2].Trim()
# Remove possible quotes
$value = $value -replace "^[`"']|[`"']$"
# Set environment variable
[Environment]::SetEnvironmentVariable($key, $value, "Process")
}
}
}
# Function: Load project JSON configuration
function Import-ProjectConfig {
param([string]$ProjectDir, [string]$ProjectJson)
$configPath = "$ProjectRoot\project_files\$ProjectDir\$ProjectJson.json"
if (-not (Test-Path $configPath)) {
Write-Log "WARNING: Project config file not found at $configPath"
if ($Mode -eq "dev") {
Write-Host "WARNING: Project config file not found at $configPath"
}
return
}
Write-Log "Loading project configuration from $configPath"
if ($Mode -eq "dev") {
Write-Host "Loading project configuration from $configPath"
}
try {
$config = Get-Content $configPath -Raw | ConvertFrom-Json
# Load environment variables from config.env section
if ($config.env) {
foreach ($key in $config.env.PSObject.Properties.Name) {
$value = $config.env.$key
if ($value -ne $null -and $value -ne "") {
[Environment]::SetEnvironmentVariable($key, $value, "Process")
Write-Log "Set environment variable: $key"
}
}
}
} catch {
Write-Log "ERROR: Failed to load project config: $_"
if ($Mode -eq "dev") {
Write-Host "ERROR: Failed to load project config: $_"
}
}
}
# Function: Check if port is in use (dev mode only)
function Test-PortInUse {
param([int]$PortNumber)
$connections = Get-NetTCPConnection -LocalPort $PortNumber -ErrorAction SilentlyContinue
if ($connections -eq $null) {
return $false
}
return $connections.Count -gt 0
}
# Function: Get process using port (dev mode only)
function Get-PortProcess {
param([int]$PortNumber)
$connection = Get-NetTCPConnection -LocalPort $PortNumber -ErrorAction SilentlyContinue | Select-Object -First 1
if ($connection -ne $null) {
try {
return Get-Process -Id $connection.OwningProcess -ErrorAction SilentlyContinue
} catch {
return $null
}
}
return $null
}
# Function: Clear port usage (dev mode only)
function Clear-Port {
param([int]$PortNumber)
if ($Mode -eq "dev") {
Write-Host "Checking if port $PortNumber is available..."
}
$process = Get-PortProcess -PortNumber $PortNumber
if ($process) {
# Skip Idle process (PID 0) as it's a system process and cannot be killed
if ($process.Id -eq 0) {
if ($Mode -eq "dev") {
Write-Host "[INFO] Found Idle process (PID: 0), skipping termination as it's a system process" -ForegroundColor Yellow
}
Write-Log "Found Idle process (PID: 0), skipping termination"
} else {
if ($Mode -eq "dev") {
Write-Host "[WARNING] Found process $($process.ProcessName) (PID: $($process.Id)) using port $PortNumber" -ForegroundColor Yellow
}
Write-Log "Found process $($process.Id) using port $PortNumber"
if ($Mode -eq "dev") {
Write-Host "Killing process $($process.Id)..."
}
try {
Stop-Process -Id $process.Id -Force -ErrorAction Stop
Write-Log "Killed process $($process.Id)"
if ($Mode -eq "dev") {
Write-Host "[INFO] Process killed successfully" -ForegroundColor Green
}
}
catch {
if ($Mode -eq "dev") {
Write-Host "[ERROR] Failed to kill process: $_" -ForegroundColor Red
}
Write-Log "Failed to kill process $($process.Id): $_"
}
}
}
# Wait for port release
Start-Sleep -Seconds 5
# Verify port is available
if (Test-PortInUse -PortNumber $PortNumber) {
# Check if it's still the Idle process
$process = Get-PortProcess -PortNumber $PortNumber
if ($process -and $process.Id -eq 0) {
# Idle process doesn't actually use the port, so consider it available
if ($Mode -eq "dev") {
Write-Host "[INFO] Port $PortNumber is available for use (Idle process detected)" -ForegroundColor Green
}
Write-Log "Port $PortNumber is available (Idle process detected)"
return $true
} else {
if ($Mode -eq "dev") {
Write-Host "[ERROR] Port $PortNumber is still in use!" -ForegroundColor Red
}
Write-Log "Port $PortNumber is still in use!"
return $false
}
}
else {
if ($Mode -eq "dev") {
Write-Host "[INFO] Port $PortNumber is available for use" -ForegroundColor Green
}
Write-Log "Port $PortNumber is available"
return $true
}
}
# Function: Find Python interpreter
function Find-Python {
$venvPython = "$ProjectRoot\venv\Scripts\python.exe"
if (Test-Path $venvPython) {
if ($Mode -eq "dev") {
Write-Host "[DEBUG] Virtual Python found: $venvPython"
}
return $venvPython
}
else {
if ($Mode -eq "dev") {
Write-Host "[DEBUG] Virtual Python not found, using system Python"
}
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
if ($pythonCmd) {
if ($Mode -eq "dev") {
Write-Host "[DEBUG] System Python found: $($pythonCmd.Source)"
}
return $pythonCmd.Source
}
throw "Python not found!"
}
}
# ==================== Main Program ====================
try {
# Create log directory
if (-not (Test-Path "$ProjectRoot\logs")) {
New-Item -ItemType Directory -Path "$ProjectRoot\logs" -Force | Out-Null
if ($Mode -eq "dev") {
Write-Host "[INFO] Logs directory created"
}
}
# Load configuration from .env file (command line parameter has highest priority)
Import-DotEnv -Path $EnvFile
# Load project configuration based on PROJECT_DIR and PROJECT_JSON
$projectDir = $env:PROJECT_DIR
$projectJson = $env:PROJECT_JSON
if ($projectDir -and $projectJson) {
Write-Log "Loading project config: $projectDir/$projectJson"
if ($Mode -eq "dev") {
Write-Host "Loading project config: $projectDir/$projectJson"
}
Import-ProjectConfig -ProjectDir $projectDir -ProjectJson $projectJson
} else {
Write-Log "WARNING: PROJECT_DIR or PROJECT_JSON not set in .env file"
if ($Mode -eq "dev") {
Write-Host "WARNING: PROJECT_DIR or PROJECT_JSON not set in .env file"
}
}
# Read default values from environment variables (if not specified in command line)
if ($PSBoundParameters.ContainsKey("Port") -eq $false -and $env:PORT) {
$Port = [int]$env:PORT
}
if ($PSBoundParameters.ContainsKey("Host") -eq $false -and $env:HOST) {
$HostAddress = $env:HOST
}
# Set environment variables
[Environment]::SetEnvironmentVariable("PORT", $Port, "Process")
[Environment]::SetEnvironmentVariable("HOST", $HostAddress, "Process")
[Environment]::SetEnvironmentVariable("START_MODE", $Mode, "Process")
[Environment]::SetEnvironmentVariable("PYTHONPATH", $ProjectRoot, "Process")
[Environment]::SetEnvironmentVariable("PYTHONUNBUFFERED", "1", "Process")
# Check if static directory exists
$staticDir = "$ProjectRoot\static"
if (Test-Path $staticDir) {
Write-Log "Static directory found: $staticDir"
if ($Mode -eq "dev") {
Write-Host "Static directory found: $staticDir"
}
} else {
Write-Log "WARNING: Static directory not found at $staticDir"
if ($Mode -eq "dev") {
Write-Host "WARNING: Static directory not found at $staticDir"
}
}
# Display configuration information (dev mode only)
if ($Mode -eq "dev") {
Write-Host ""
Write-Host "FastAPI Server Starting..." -ForegroundColor Cyan
Write-Host "Port: $Port, Host: $HostAddress"
Write-Host "Project Root: $ProjectRoot" -ForegroundColor Yellow
Write-Host "Mode: $Mode"
Write-Host "Press Ctrl+C to stop gracefully"
Write-Host ""
}
# Record startup log
Write-Log "Server starting on port $Port, host $HostAddress, mode $Mode"
# Check port availability (dev mode only)
if ($Mode -eq "dev") {
Write-Host "[INFO] Checking port availability..."
Clear-Port -PortNumber $Port
}
# Find Python
try {
$Python = Find-Python
Write-Log "Found Python: $Python"
if ($Mode -eq "dev") {
Write-Host "Python: $Python"
}
} catch {
Write-Log "Failed to find Python: $_"
if ($Mode -eq "dev") {
Write-Host "[ERROR] Failed to find Python: $_" -ForegroundColor Red
}
exit 1
}
# Build uvicorn arguments
$uvicornArgs = @(
"-m", "uvicorn",
$App,
"--host", $HostAddress,
"--port", "$Port",
"--log-level", "info",
"--access-log"
)
Write-Log "Starting uvicorn server: $Python $($uvicornArgs -join ' '); Working directory: $ProjectRoot"
if ($Mode -eq "dev") {
Write-Host ""
Write-Host "Starting uvicorn server..." -ForegroundColor Cyan
Write-Host "Uvicorn command: $Python $($uvicornArgs -join ' ')" -ForegroundColor Yellow
}
# Start server
if ($Mode -eq "service") {
# Service mode: run directly
try {
& $Python $uvicornArgs
} catch {
Write-Log "Failed to start server: $_"
exit 1
}
} else {
# Dev mode: run with graceful shutdown support
try {
$serverProcess = Start-Process -FilePath $Python -ArgumentList $uvicornArgs -NoNewWindow -PassThru
if ($Mode -eq "dev") {
Write-Host "Server process started with PID: $($serverProcess.Id)" -ForegroundColor Green
}
Write-Log "Server process started with PID: $($serverProcess.Id)"
} catch {
Write-Log "Failed to start server process: $_"
if ($Mode -eq "dev") {
Write-Host "[ERROR] Failed to start server process: $_" -ForegroundColor Red
}
exit 1
}
# Setup graceful shutdown handler
$shutdownRequested = $false
$null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
$script:shutdownRequested = $true
}
try {
# Wait for process to exit
if ($Mode -eq "dev") {
Write-Host "[DEBUG] Waiting for server process to exit..."
}
Wait-Process -Id $serverProcess.Id -ErrorAction Stop
} catch {
if ($shutdownRequested -or $Error[0].Exception.Message -match "was not found") {
if ($Mode -eq "dev") {
Write-Host ""
Write-Host "Shutting down server gracefully..." -ForegroundColor Yellow
}
Write-Log "Shutting down server gracefully"
# Try graceful shutdown first
$serverProcess.CloseMainWindow() | Out-Null
Start-Sleep -Seconds 5
# Force kill if still running
if (!$serverProcess.HasExited) {
if ($Mode -eq "dev") {
Write-Host "Force killing server process..." -ForegroundColor Yellow
}
$serverProcess | Stop-Process -Force -ErrorAction SilentlyContinue
}
}
else {
if ($Mode -eq "dev") {
Write-Host ""
Write-Host "[ERROR] Server process error: $_" -ForegroundColor Red
}
Write-Log "Server process error: $_"
}
}
# Get actual exit code
$exitCode = $serverProcess.ExitCode
# Check exit code
if ($exitCode -ne 0) {
if ($Mode -eq "dev") {
Write-Host ""
Write-Host "[ERROR] Application failed to start with error code $exitCode" -ForegroundColor Red
}
Write-Log "Application failed to start with error code $exitCode"
exit $exitCode
}
}
} catch {
Write-Log "Error: $_"
if ($Mode -eq "dev") {
Write-Host ""
Write-Host "[ERROR] An error occurred: $_" -ForegroundColor Red
}
exit 1
} finally {
# Cleanup work (dev mode only)
if ($Mode -eq "dev") {
Write-Host ""
Write-Host "Server shutting down..." -ForegroundColor Yellow
}
Write-Log "Server shutting down"
# Clear port usage (dev mode only)
if ($Mode -eq "dev") {
Write-Host "Cleaning up any remaining processes on port $Port..."
$process = Get-PortProcess -PortNumber $Port
if ($process) {
try {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
if ($Mode -eq "dev") {
Write-Host "Killed process $($process.Id)"
}
Write-Log "Killed process $($process.Id) during cleanup"
} catch {
# Ignore errors
}
}
Write-Host "Shutdown complete." -ForegroundColor Green
}
Write-Log "Server shutdown complete"
}
+170
View File
@@ -0,0 +1,170 @@
@echo off
rem Simple service management script
setlocal
set "SCRIPT_DIR=%~dp0"
set "NSSM_EXE=%SCRIPT_DIR%nssm.exe"
set "SERVICE_NAME=MyAPS_API"
set "RUN_PS1=%SCRIPT_DIR%run.ps1"
rem Calculate project root (parent directory of scripts)
for %%i in ("%SCRIPT_DIR%..") do set "PROJECT_ROOT=%%~fi"
set "LOG_DIR=%PROJECT_ROOT%\logs"
rem Check if NSSM exists
if not exist "%NSSM_EXE%" (
echo ERROR: NSSM not found at %NSSM_EXE%
echo Please download NSSM and place it in the scripts directory
pause
exit /b 1
)
rem Check if run.ps1 exists
if not exist "%RUN_PS1%" (
echo ERROR: run.ps1 not found at %RUN_PS1%
echo Please create run.ps1 script in the scripts directory
pause
exit /b 1
)
rem Check admin rights
net session >nul 2>&1
if %errorLevel% neq 0 (
echo ERROR: Please run as administrator
pause
exit /b 1
)
:MENU
cls
echo FastAPI Service Management
echo 1. Install service
echo 2. Start service
echo 3. Stop service
echo 4. Restart service
echo 5. Uninstall service
echo 6. Check status
echo 7. Test run.ps1 (dev mode)
echo 8. Test run.ps1 (service mode)
echo 0. Exit
echo.
set /p choice=Enter choice:
if "%choice%"=="1" goto :INSTALL
if "%choice%"=="2" goto :START
if "%choice%"=="3" goto :STOP
if "%choice%"=="4" goto :RESTART
if "%choice%"=="5" goto :UNINSTALL
if "%choice%"=="6" goto :STATUS
if "%choice%"=="7" goto :TEST_RUNPS1_DEV
if "%choice%"=="8" goto :TEST_RUNPS1_SERVICE
if "%choice%"=="0" goto :EXIT
echo Invalid choice
pause
goto :MENU
:INSTALL
echo Installing service...
echo Using run.ps1: %RUN_PS1%
echo Project root: %PROJECT_ROOT%
rem Create logs directory
if not exist "%LOG_DIR%" (
echo Creating logs directory...
mkdir "%LOG_DIR%"
)
echo Installing service with AppDirectory: %PROJECT_ROOT%
"%NSSM_EXE%" install "%SERVICE_NAME%" powershell.exe
"%NSSM_EXE%" set "%SERVICE_NAME%" AppParameters "-ExecutionPolicy Bypass -NoProfile -File %RUN_PS1% -Mode service"
"%NSSM_EXE%" set "%SERVICE_NAME%" AppDirectory "%PROJECT_ROOT%"
"%NSSM_EXE%" set "%SERVICE_NAME%" Start SERVICE_AUTO_START
"%NSSM_EXE%" set "%SERVICE_NAME%" AppStdout "%LOG_DIR%\nssm_stdout.log"
"%NSSM_EXE%" set "%SERVICE_NAME%" AppStderr "%LOG_DIR%\nssm_stderr.log"
echo Service installed
echo Logs will be saved to: %LOG_DIR%
pause
goto :MENU
:START
echo Starting service...
"%NSSM_EXE%" start "%SERVICE_NAME%"
if %errorLevel% neq 0 (
echo ERROR: Failed to start service
echo Possible reasons:
echo 1. run.ps1 script has errors
echo 2. PowerShell execution policy issues
echo 3. Missing dependencies
echo 4. Working directory issues
echo.
echo Check logs at: %LOG_DIR%
)
pause
goto :MENU
:STOP
echo Stopping service...
"%NSSM_EXE%" stop "%SERVICE_NAME%"
pause
goto :MENU
:RESTART
echo Restarting service...
"%NSSM_EXE%" restart "%SERVICE_NAME%"
pause
goto :MENU
:UNINSTALL
echo Uninstalling service...
"%NSSM_EXE%" stop "%SERVICE_NAME%" >nul 2>&1
"%NSSM_EXE%" remove "%SERVICE_NAME%" confirm
pause
goto :MENU
:STATUS
echo Checking status...
"%NSSM_EXE%" status "%SERVICE_NAME%"
echo.
echo Service configuration:
"%NSSM_EXE%" get "%SERVICE_NAME%" AppParameters
"%NSSM_EXE%" get "%SERVICE_NAME%" AppDirectory
pause
goto :MENU
:TEST_RUNPS1_DEV
echo Testing run.ps1 script in dev mode...
echo Running: %RUN_PS1% -Mode dev
echo Working directory: %PROJECT_ROOT%
echo.
echo Output:
echo ----------------------------------------
powershell.exe -ExecutionPolicy Bypass -NoProfile -File %RUN_PS1% -Mode dev
echo ----------------------------------------
echo.
echo Test completed.
echo If you see errors above, fix them before starting the service.
pause
goto :MENU
:TEST_RUNPS1_SERVICE
echo Testing run.ps1 script in service mode...
echo Running: %RUN_PS1% -Mode service
echo Working directory: %PROJECT_ROOT%
echo.
echo Output:
echo ----------------------------------------
powershell.exe -ExecutionPolicy Bypass -NoProfile -File %RUN_PS1% -Mode service
echo ----------------------------------------
echo.
echo Test completed.
echo If you see errors above, fix them before starting the service.
pause
goto :MENU
:EXIT
echo Exiting...
pause
+45 -7
View File
@@ -1003,6 +1003,44 @@ function formatRelativeTime(timestamp) {
return Math.floor(diff / 86400) + '天前';
}
// 格式化相对日期时间(今天,昨天,前天,早于前天才使用具体日期时间)
function formatRelativeDate(timestamp) {
const now = new Date();
const date = new Date(timestamp * 1000);
// 今天
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
// 昨天
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
// 前天
const dayBeforeYesterday = new Date(today);
dayBeforeYesterday.setDate(dayBeforeYesterday.getDate() - 2);
const logDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
// 判断是否是今天、昨天、前天
let dayLabel = '';
if (logDate.getTime() === today.getTime()) {
dayLabel = '今天';
} else if (logDate.getTime() === yesterday.getTime()) {
dayLabel = '昨天';
} else if (logDate.getTime() === dayBeforeYesterday.getTime()) {
dayLabel = '前天';
} else {
// 显示具体日期
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
dayLabel = `${month}-${day}`;
}
// 显示具体时间
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
return `${dayLabel} ${hours}:${minutes}:${seconds}`;
}
// 刷新事件统计
function refreshEventStats() {
fetchEventStats();
@@ -1191,8 +1229,7 @@ function updateAPIRequestsDisplay(data) {
}
tbodyEl.innerHTML = filteredRequests.map((req, index) => {
const date = new Date(req.timestamp * 1000);
const timeStr = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}:${date.getSeconds().toString().padStart(2,'0')}`;
const timeStr = formatRelativeDate(req.timestamp);
const methodClass = req.method.toLowerCase();
const isSuccess = req.status_code < 400;
@@ -1587,8 +1624,7 @@ function updateLogsPageDisplay(logs) {
}
tbodyEl.innerHTML = filteredLogs.map((log, index) => {
const date = new Date(log.timestamp * 1000);
const timeStr = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}:${date.getSeconds().toString().padStart(2,'0')}`;
const timeStr = formatRelativeDate(log.timestamp);
// 确保 title 属性使用完整的消息内容
const fullMessage = log.message || '';
@@ -1609,12 +1645,15 @@ function updateLogsPageDisplay(logs) {
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 移除模块名称中的"smart_"前缀
const moduleName = log.module.replace(/^smart_/, '');
return `
<tr data-log-id="${logId}" data-log-index="${index}" class="${readStatusClass}" onclick="showLogDetail(${index})">
<td class="font-mono">${index + 1}</td>
<td>${timeStr}</td>
<td><span class="log-level-badge ${log.level}">${log.level.toUpperCase()}</span></td>
<td>${log.module}</td>
<td>${moduleName}</td>
<td class="log-message-cell" title="${escapedFullMessage}" data-full-message="${escapedFullMessage}">${escapeHtml(log.message || '')}</td>
<td class="read-status-cell">
<span class="read-checkbox ${readStatusClass}" onclick="toggleLogReadStatus('${logId}'); event.stopPropagation();">${readIcon}</span>
@@ -2282,8 +2321,7 @@ function getStatusDescription(statusCode) {
// 格式化发送请求行
function formatOutboundRequestRow(request, index) {
const date = new Date(request.timestamp * 1000);
const timestamp = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}:${date.getSeconds().toString().padStart(2,'0')}`;
const timestamp = formatRelativeDate(request.timestamp);
const duration = (request.duration * 1000).toFixed(0);
const statusClass = request.status_code >= 400 ? 'error' : 'success';
const durationClass = request.duration > 1 ? 'slow' : '';