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>
268 lines
9.7 KiB
PowerShell
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
|
|
}
|