Files
goauto/scripts/start-server.ps1
T
QiuSWandClaude Opus 5 bbc5fdba05 fix(#48): replace per-migration noise with a summary, force UTF-8 console
The startup output was dominated by two upstream behaviours. Migrate
printed a bare count for every already-applied migration — a lone "1"
with no version, no explanation — through the standard logger, hence
stderr, which the launcher's `2>&1 |` renders as a red PowerShell error
block that looks like a failed migration. And the version-existence check
echoed one SELECT count(*) per version. Both replaced: a Warn-level
session for the check, and one summary line on stdout.

Console encoding needed more than [Console]::OutputEncoding: PowerShell
5.1 decodes child-process output by the console code page, so chcp 65001
goes with it.

The regression test capture was wrong twice and mutation testing caught
both. It captured only stdout while the bug wrote to stderr, and it
compared lines to "1" when the logger prefixes a timestamp, so the
assertion could never fail. It now captures the standard logger too and
matches with a pattern that allows the prefix — verified by reinstating
the bug and watching the test fail.

Not verified: the PowerShell edits, which need a Windows run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:53:56 +08:00

262 lines
9.5 KiB
PowerShell

[CmdletBinding()]
param(
[string]$ConfigPath,
[string]$DatabaseHost,
[Nullable[int]]$DatabasePort,
[string]$DatabaseUser,
[string]$DatabaseName,
[switch]$SkipMigration,
[switch]$ValidateConfigOnly
)
$ErrorActionPreference = "Stop"
# Go 进程输出 UTF-8。不显式设定的话,中文日志会按控制台的 ANSI 代码页(简中为
# GBK)解码,显示成乱码——见 #48 启动日志。
#
# 只设 [Console]::OutputEncoding 不够:PowerShell 5.1 解码子进程输出时还要看
# 控制台代码页,所以两个都得改。chcp 的输出丢弃掉,避免多打一行。
try {
$null = & chcp.com 65001
[Console]::OutputEncoding = [Text.Encoding]::UTF8
$OutputEncoding = [Text.Encoding]::UTF8
} catch { }
$workspaceRoot = Split-Path -Parent $PSScriptRoot
$serverDirectory = Join-Path $workspaceRoot "server"
$migrationLog = Join-Path $serverDirectory "temp\startup-migration.log"
$plainPassword = $null
function ConvertFrom-YamlScalar {
param([string]$Value)
$value = $Value.Trim()
if ($value.Length -ge 2) {
if ($value.StartsWith('"') -and $value.EndsWith('"')) {
return $value.Substring(1, $value.Length - 2).Replace('\"', '"').Replace('\\', '\')
}
if ($value.StartsWith("'") -and $value.EndsWith("'")) {
return $value.Substring(1, $value.Length - 2).Replace("''", "'")
}
}
return $value
}
function Read-DatabaseConfig {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
throw "Local configuration file was not found: $Path. Copy config.example.yaml to config.yaml and fill in database.password."
}
$values = @{}
$insideDatabase = $false
foreach ($line in Get-Content -LiteralPath $Path) {
if ($line -match '^\s*(#.*)?$') {
continue
}
if ($line -match '^database\s*:\s*$') {
$insideDatabase = $true
continue
}
if ($insideDatabase -and $line -match '^\S') {
break
}
if ($insideDatabase -and $line -match '^\s+(host|port|user|password|name)\s*:\s*(.*?)\s*$') {
$values[$Matches[1]] = ConvertFrom-YamlScalar $Matches[2]
}
}
foreach ($key in 'host', 'port', 'user', 'password', 'name') {
if (-not $values.ContainsKey($key) -or [string]::IsNullOrWhiteSpace([string]$values[$key])) {
throw "Missing database.$key in local configuration file: $Path"
}
}
$parsedPort = 0
if (-not [int]::TryParse($values.port, [ref]$parsedPort) -or $parsedPort -lt 1 -or $parsedPort -gt 65535) {
throw "database.port must be an integer between 1 and 65535 in: $Path"
}
if ($values.user -notmatch '^[A-Za-z0-9_.-]+$') {
throw "database.user contains unsupported characters in: $Path"
}
if ($values.name -notmatch '^[A-Za-z0-9_]+$') {
throw "database.name contains unsupported characters in: $Path"
}
return @{
Host = [string]$values.host
Port = $parsedPort
User = [string]$values.user
Password = [string]$values.password
Name = [string]$values.name
}
}
function Read-PortConfig {
param([string]$Path)
$values = @{}
$insidePorts = $false
foreach ($line in Get-Content -LiteralPath $Path) {
if ($line -match '^\s*(#.*)?$') {
continue
}
if ($line -match '^ports\s*:\s*$') {
$insidePorts = $true
continue
}
if ($insidePorts -and $line -match '^\S') {
break
}
if ($insidePorts -and $line -match '^\s+(server|web)\s*:\s*(.*?)\s*$') {
$values[$Matches[1]] = ConvertFrom-YamlScalar $Matches[2]
}
}
foreach ($key in 'server', 'web') {
$parsedPort = 0
if (-not $values.ContainsKey($key) -or
-not [int]::TryParse([string]$values[$key], [ref]$parsedPort) -or
$parsedPort -lt 1 -or $parsedPort -gt 65535) {
throw "ports.$key must be an integer between 1 and 65535 in: $Path"
}
$values[$key] = $parsedPort
}
if ($values.server -eq $values.web) {
throw "ports.server and ports.web must be different in: $Path"
}
return @{ Server = [int]$values.server; Web = [int]$values.web }
}
function Resolve-MySqlClient {
$services = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue |
Where-Object { $_.Name -match '^MySQL' -and $_.State -eq 'Running' }
foreach ($service in $services) {
if ($service.PathName -match '^"?(.*?\\mysqld\.exe)"?(?:\s|$)') {
$serviceClient = Join-Path (Split-Path -Parent $Matches[1]) 'mysql.exe'
if (Test-Path -LiteralPath $serviceClient) {
return $serviceClient
}
}
}
$defaultPath = "C:\Program Files\MySQL\MySQL Server 8.4\bin\mysql.exe"
if (Test-Path -LiteralPath $defaultPath) {
return $defaultPath
}
$command = Get-Command mysql.exe -ErrorAction SilentlyContinue
if ($command) {
return $command.Source
}
throw "mysql.exe was not found. Add the MySQL 8.4 bin directory to PATH."
}
try {
if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
$ConfigPath = Join-Path $workspaceRoot "config.yaml"
}
elseif (-not [IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path $workspaceRoot $ConfigPath
}
$ConfigPath = [IO.Path]::GetFullPath($ConfigPath)
$databaseConfig = Read-DatabaseConfig $ConfigPath
$portConfig = Read-PortConfig $ConfigPath
if (-not $PSBoundParameters.ContainsKey('DatabaseHost')) {
$DatabaseHost = $databaseConfig.Host
}
if (-not $PSBoundParameters.ContainsKey('DatabasePort')) {
$DatabasePort = $databaseConfig.Port
}
if (-not $PSBoundParameters.ContainsKey('DatabaseUser')) {
$DatabaseUser = $databaseConfig.User
}
if (-not $PSBoundParameters.ContainsKey('DatabaseName')) {
$DatabaseName = $databaseConfig.Name
}
$plainPassword = $databaseConfig.Password
Write-Host "GoAuto local server" -ForegroundColor Cyan
Write-Host "Config: $ConfigPath"
Write-Host "MySQL: ${DatabaseUser}@${DatabaseHost}:${DatabasePort}/${DatabaseName}"
Write-Host "Ports: server=$($portConfig.Server), web=$($portConfig.Web)"
if ($ValidateConfigOnly) {
Write-Host "Local database and port configuration is valid." -ForegroundColor Green
return
}
$mysqlClient = Resolve-MySqlClient
$env:MYSQL_PWD = $plainPassword
try {
Write-Host "[1/3] Checking database ${DatabaseName}..." -ForegroundColor Cyan
& $mysqlClient `
--protocol=TCP `
--host=$DatabaseHost `
--port=$DatabasePort `
--user=$DatabaseUser `
--default-character-set=utf8mb4 `
--execute="CREATE DATABASE IF NOT EXISTS ``${DatabaseName}`` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
if ($LASTEXITCODE -ne 0) {
throw "MySQL connection or database creation failed."
}
}
finally {
Remove-Item Env:MYSQL_PWD -ErrorAction SilentlyContinue
}
# 让服务端自己读同一个 config.yaml,取出 syb 凭据等本地配置。
# 下面的 GOAUTO_* 变量优先级高于文件,所以数据库和端口仍以脚本算出的为准。
$env:GOAUTO_CONFIG = $ConfigPath
$env:GOAUTO_DB_DRIVER = "mysql"
$env:GOAUTO_DB_DSN = "${DatabaseUser}:${plainPassword}@tcp(${DatabaseHost}:${DatabasePort})/${DatabaseName}?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s"
$env:GOAUTO_SERVER_PORT = [string]$portConfig.Server
Push-Location $serverDirectory
try {
if (-not $SkipMigration) {
Write-Host "[2/3] Running database migrations..." -ForegroundColor Cyan
New-Item -ItemType Directory -Path (Split-Path -Parent $migrationLog) -Force | Out-Null
$previousErrorActionPreference = $ErrorActionPreference
try {
$ErrorActionPreference = "Continue"
& go run . migrate -c config/settings.yml 2>&1 |
Tee-Object -FilePath $migrationLog
$migrationExitCode = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $previousErrorActionPreference
}
if ($migrationExitCode -ne 0) {
$migrationTail = (Get-Content -LiteralPath $migrationLog -Tail 40 -ErrorAction SilentlyContinue) -join [Environment]::NewLine
throw "Database migration failed. Log: ${migrationLog}`n${migrationTail}"
}
}
else {
Write-Host "[2/3] Database migration skipped." -ForegroundColor DarkYellow
}
Write-Host "[3/3] Starting server..." -ForegroundColor Green
Write-Host "Local URL: http://127.0.0.1:$($portConfig.Server)"
Write-Host "Android URL: use this computer's LAN IP on port $($portConfig.Server)"
Write-Host "Press Ctrl+C to stop." -ForegroundColor DarkYellow
& go run . server -c config/settings.yml
if ($LASTEXITCODE -ne 0) {
throw "Server exited with code $LASTEXITCODE."
}
}
finally {
Pop-Location
}
}
finally {
Remove-Item Env:MYSQL_PWD -ErrorAction SilentlyContinue
Remove-Item Env:GOAUTO_DB_DSN -ErrorAction SilentlyContinue
Remove-Item Env:GOAUTO_DB_DRIVER -ErrorAction SilentlyContinue
Remove-Item Env:GOAUTO_SERVER_PORT -ErrorAction SilentlyContinue
Remove-Item Env:GOAUTO_CONFIG -ErrorAction SilentlyContinue
$plainPassword = $null
}