[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" # Stringify before Tee-Object. With a bare `2>&1 |`, PowerShell wraps # every line the child writes to stderr in an ErrorRecord, which prints # as a red NativeCommandError block and reads as a failed migration -- # even for ordinary informational output such as go-admin's # "config init". Chasing individual stderr writers is whack-a-mole; this # fixes the whole class. $LASTEXITCODE still reports the child's real # exit code through the added pipeline stage, so failure detection below # is unaffected. & go run . migrate -c config/settings.yml 2>&1 | ForEach-Object { "$_" } | 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 }