The startup log proved config.yaml was not being found during migrate. Lookup covered the working directory and the executable's directory, but the server runs from server/ while config.yaml sits at the repository root — so it was found only when a launcher happened to export GOAUTO_CONFIG. Anyone running `go run .` by hand got no local config at all. Search the parent directory too, with the working directory still winning. The diagnostics also wrote to stderr, and the launcher pipes the server through `2>&1 | Tee-Object`, which turns every stderr write into a PowerShell NativeCommandError. The informational line I added to make this debuggable was itself rendering as a red error block. They go to stdout now. Launchers set the console to UTF-8: Go writes UTF-8 while the console decodes as the ANSI code page, which turned every Chinese log line into mojibake. Verified by running the built binary from server/ with no GOAUTO_CONFIG set: it loads ../config.yaml and the lines survive 2>/dev/null. Not verified: the two PowerShell edits, which need a Windows run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
255 lines
9.2 KiB
PowerShell
255 lines
9.2 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 启动日志。
|
|
try { [Console]::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
|
|
}
|