diff --git a/scripts/download_wheels.ps1 b/scripts/download_wheels.ps1 index e2b9e53..eef4d8a 100644 --- a/scripts/download_wheels.ps1 +++ b/scripts/download_wheels.ps1 @@ -1,7 +1,55 @@ +param( + [switch]$NoPause +) + $ErrorActionPreference = "Stop" $Root = Split-Path -Parent $PSScriptRoot $WheelDir = Join-Path $Root "vendor\wheels" $Req = Join-Path $Root "requirements.txt" -New-Item -ItemType Directory -Force -Path $WheelDir | Out-Null -python -m pip download -r $Req -d $WheelDir -Write-Host "依赖包已下载到 $WheelDir" + +function Wait-BeforeExit { + if (-not $NoPause) { + Write-Host "" + Read-Host "Press Enter to exit" + } +} + +function Get-PythonCommand { + foreach ($name in @("py", "python", "python3")) { + if (Get-Command $name -ErrorAction SilentlyContinue) { + return $name + } + } + throw "Python was not found. Install Python 3.10+ before downloading wheels." +} + +try { + Set-Location $Root + if (-not (Test-Path $Req)) { + throw "requirements.txt was not found: $Req" + } + + $python = Get-PythonCommand + New-Item -ItemType Directory -Force -Path $WheelDir | Out-Null + + Write-Host "Downloading offline wheels to: $WheelDir" -ForegroundColor Cyan + if ($python -eq "py") { + & py -3 -m pip download -r $Req -d $WheelDir + } else { + & $python -m pip download -r $Req -d $WheelDir + } + + if ($LASTEXITCODE -ne 0) { + throw "Wheel download failed. Check network access and pip." + } + + Write-Host "" + Write-Host "Offline wheels downloaded. Copy the whole project folder to the server and run scripts\install_offline.ps1." -ForegroundColor Green +} catch { + Write-Host "" + Write-Host "Download failed:" -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + exit 1 +} finally { + Wait-BeforeExit +} diff --git a/scripts/install_offline.ps1 b/scripts/install_offline.ps1 index fa21a7c..e62084c 100644 --- a/scripts/install_offline.ps1 +++ b/scripts/install_offline.ps1 @@ -1,19 +1,165 @@ +param( + [switch]$NoPause +) + $ErrorActionPreference = "Stop" $Root = Split-Path -Parent $PSScriptRoot $Venv = Join-Path $Root ".venv" -$Python = "python" - -if (-not (Test-Path $Venv)) { - & $Python -m venv $Venv -} - $VenvPython = Join-Path $Venv "Scripts\python.exe" $WheelDir = Join-Path $Root "vendor\wheels" $Req = Join-Path $Root "requirements.txt" -if (-not (Test-Path $WheelDir)) { - throw "未找到离线依赖目录:$WheelDir。请先在联网环境运行 scripts\download_wheels.ps1。" +function Write-Step { + param([string]$Message) + Write-Host "" + Write-Host "==> $Message" -ForegroundColor Cyan } -& $VenvPython -m pip install --no-index --find-links $WheelDir -r $Req -Write-Host "安装完成。运行 scripts\run.ps1 启动服务。" +function Wait-BeforeExit { + if (-not $NoPause) { + Write-Host "" + Read-Host "Press Enter to exit" + } +} + +function Get-PythonCandidate { + $candidates = @( + @{ Exe = "py"; Args = @("-3") }, + @{ Exe = "python"; Args = @() }, + @{ Exe = "python3"; Args = @() } + ) + + foreach ($candidate in $candidates) { + $command = Get-Command $candidate.Exe -ErrorAction SilentlyContinue + if (-not $command) { + continue + } + + try { + $args = @($candidate.Args) + @("-c", "import sys; print(sys.executable); print(sys.version_info.major); print(sys.version_info.minor)") + $output = & $candidate.Exe @args 2>$null + if ($LASTEXITCODE -ne 0 -or $output.Count -lt 3) { + continue + } + $major = [int]$output[1] + $minor = [int]$output[2] + if ($major -gt 3 -or ($major -eq 3 -and $minor -ge 10)) { + return [pscustomobject]@{ + Exe = $candidate.Exe + Args = $candidate.Args + Path = $output[0] + Version = "$major.$minor" + } + } + } catch { + continue + } + } + + return $null +} + +function Install-PythonWithWinget { + $winget = Get-Command winget -ErrorAction SilentlyContinue + if (-not $winget) { + throw "Python 3.10+ was not found, and winget is not available. Install Python 3.10+ manually and run this script again." + } + + Write-Step "Python was not found. Installing Python 3.12 with winget" + & winget install --id Python.Python.3.12 -e --source winget --accept-package-agreements --accept-source-agreements + if ($LASTEXITCODE -ne 0) { + throw "winget failed to install Python. Check network access or install Python 3.10+ manually." + } +} + +function Ensure-Python { + $python = Get-PythonCandidate + if ($python) { + Write-Host "Python found: $($python.Path), version $($python.Version)" + return $python + } + + Install-PythonWithWinget + $python = Get-PythonCandidate + if (-not $python) { + throw "Python is still unavailable after installation. Reopen PowerShell or check PATH." + } + Write-Host "Python ready: $($python.Path), version $($python.Version)" + return $python +} + +function Invoke-Python { + param( + [object]$Python, + [string[]]$Arguments + ) + $allArgs = @($Python.Args) + $Arguments + & $Python.Exe @allArgs + if ($LASTEXITCODE -ne 0) { + throw "Python command failed: $($Python.Exe) $($allArgs -join ' ')" + } +} + +function Ensure-Venv { + param([object]$Python) + if (-not (Test-Path $VenvPython)) { + Write-Step "Creating virtual environment" + Invoke-Python -Python $Python -Arguments @("-m", "venv", $Venv) + } else { + Write-Host "Virtual environment already exists: $Venv" + } + + if (-not (Test-Path $VenvPython)) { + throw "Failed to create virtual environment: $VenvPython does not exist." + } +} + +function Install-Requirements { + if (-not (Test-Path $Req)) { + throw "requirements.txt was not found: $Req" + } + + Write-Step "Installing dependencies" + $wheels = @() + if (Test-Path $WheelDir) { + $wheels = Get-ChildItem -Path $WheelDir -Filter *.whl -File -ErrorAction SilentlyContinue + } + + if ($wheels.Count -gt 0) { + Write-Host "Offline wheel directory found: $WheelDir" + & $VenvPython -m pip install --no-index --find-links $WheelDir -r $Req + } else { + Write-Host "No offline wheels found. Installing dependencies from the network." + & $VenvPython -m pip install -r $Req + } + + if ($LASTEXITCODE -ne 0) { + throw "Dependency installation failed. If the server is offline, run scripts\download_wheels.ps1 on an online machine first and copy vendor\wheels to this server." + } +} + +function Test-ApplicationImport { + Write-Step "Verifying environment" + & $VenvPython -c "import fastapi, uvicorn, xlrd, xlwt, xlutils, httpx, yaml; print('Dependency verification passed')" + if ($LASTEXITCODE -ne 0) { + throw "Dependency verification failed." + } +} + +try { + Set-Location $Root + Write-Step "Preparing PCFAnalysis environment" + $python = Ensure-Python + Ensure-Venv -Python $python + Install-Requirements + Test-ApplicationImport + Write-Host "" + Write-Host "Environment installation completed. Run scripts\run.ps1 to start the service." -ForegroundColor Green +} catch { + Write-Host "" + Write-Host "Installation failed:" -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + exit 1 +} finally { + Wait-BeforeExit +} diff --git a/scripts/run.ps1 b/scripts/run.ps1 index 38b5a5e..a977198 100644 --- a/scripts/run.ps1 +++ b/scripts/run.ps1 @@ -1,8 +1,67 @@ +param( + [int]$Port = 7001, + [string]$HostAddress = "0.0.0.0", + [switch]$NoPause +) + $ErrorActionPreference = "Stop" $Root = Split-Path -Parent $PSScriptRoot $VenvPython = Join-Path $Root ".venv\Scripts\python.exe" -if (-not (Test-Path $VenvPython)) { - throw "未找到虚拟环境。请先运行 scripts\install_offline.ps1。" +$InstallScript = Join-Path $PSScriptRoot "install_offline.ps1" + +function Wait-BeforeExit { + if (-not $NoPause) { + Write-Host "" + Read-Host "Press Enter to exit" + } +} + +function Test-PortAvailable { + param([int]$CheckPort) + $listener = Get-NetTCPConnection -LocalPort $CheckPort -State Listen -ErrorAction SilentlyContinue + if ($listener) { + $pids = ($listener | Select-Object -ExpandProperty OwningProcess -Unique) -join ", " + throw "Port $CheckPort is already in use. Owning process PID: $pids" + } +} + +function Test-EnvironmentReady { + if (-not (Test-Path $VenvPython)) { + return $false + } + & $VenvPython -c "import fastapi, uvicorn, xlrd, xlwt, xlutils, httpx, yaml" 2>$null + return ($LASTEXITCODE -eq 0) +} + +try { + Set-Location $Root + + if (-not (Test-EnvironmentReady)) { + Write-Host "Runtime environment is missing or incomplete. Installing now..." -ForegroundColor Yellow + powershell -NoProfile -ExecutionPolicy Bypass -File $InstallScript -NoPause + if ($LASTEXITCODE -ne 0) { + throw "Automatic environment installation failed. See the error above." + } + } + + Test-PortAvailable -CheckPort $Port + + Write-Host "" + Write-Host "Starting PCFAnalysis service" -ForegroundColor Cyan + Write-Host "Bind URL: http://$HostAddress`:$Port" + Write-Host "Local URL: http://127.0.0.1`:$Port" + Write-Host "Press Ctrl+C in this window to stop the service." + Write-Host "" + + & $VenvPython -m uvicorn app.main:app --host $HostAddress --port $Port + if ($LASTEXITCODE -ne 0) { + throw "Service exited with code: $LASTEXITCODE" + } +} catch { + Write-Host "" + Write-Host "Startup failed:" -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + exit 1 +} finally { + Wait-BeforeExit } -Set-Location $Root -& $VenvPython -m uvicorn app.main:app --host 127.0.0.1 --port 7001