Files
goauto/scripts/start-server.ps1
T
QiuSWandClaude Opus 5 1d9dbfda76 fix(#48): keep PowerShell scripts ASCII — Chinese comments broke parsing
PowerShell 5.1 reads a .ps1 with no BOM using the ANSI code page. Every
byte of a UTF-8 Chinese character is >= 0x80, so GBK pairs them up two at
a time; a comment line carrying an odd number of those bytes pairs its
last byte with the trailing newline and swallows it, folding the next
line into the comment. That commented out the `try {` I had added and
left `} catch { }` orphaned, which is the parse error reported at
startup — and it shifted the line numbers, which is why the earlier
PowerShell error positions never matched the file.

My previous Chinese comment survived only because its byte count happened
to be even. Both scripts are back to ASCII, matching the English already
used throughout them, with a note saying why it matters.

Also restores CRLF: .gitattributes marks *.ps1 eol=crlf, and rewriting
these files from Python had left them LF in the working tree.

Verified this time rather than handed over untested: Windows PowerShell
is reachable from WSL, so both scripts were parse-checked and run with
-ValidateConfigOnly, and the chcp block was executed on its own
(code page 65001, console encoding utf-8).

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

268 lines
9.7 KiB
PowerShell

[CmdletBinding()]
param(
[string]$ConfigPath,
[string]$DatabaseHost,
[Nullable[int]]$DatabasePort,
[string]$DatabaseUser,
[string]$DatabaseName,
[switch]$SkipMigration,
[switch]$ValidateConfigOnly
)
$ErrorActionPreference = "Stop"
# The Go processes emit UTF-8. Without this the console decodes their output
# using the ANSI code page (936 on Simplified Chinese systems) and every
# non-ASCII log line arrives as mojibake.
#
# Setting [Console]::OutputEncoding alone is not enough: PowerShell 5.1 decodes
# child process output by the console code page, so chcp has to change too.
#
# NOTE: keep this file pure ASCII. PowerShell 5.1 reads a .ps1 without a BOM as
# ANSI, and a comment line holding an odd number of UTF-8 high bytes pairs its
# last byte with the newline, swallowing it and commenting out the line below.
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
}
# Let the server read the same config.yaml for local values such as the SYB
# credentials. The GOAUTO_* variables below outrank the file, so the
# database and port stay exactly what this script computed.
$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
}