fix(#48): find config.yaml from server/ and stop diagnostics looking like errors
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>
This commit is contained in:
@@ -10,6 +10,10 @@ param(
|
||||
)
|
||||
|
||||
$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"
|
||||
|
||||
@@ -5,6 +5,10 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Go 进程输出 UTF-8。不显式设定的话,中文日志会按控制台的 ANSI 代码页(简中为
|
||||
# GBK)解码,显示成乱码——见 #48 启动日志。
|
||||
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch { }
|
||||
$workspaceRoot = Split-Path -Parent $PSScriptRoot
|
||||
$webDirectory = Join-Path $workspaceRoot "web"
|
||||
|
||||
|
||||
+24
-6
@@ -2,11 +2,11 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdkconfig "github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -34,6 +34,14 @@ func LocalConfigPath() string {
|
||||
if _, err := os.Stat(LocalConfigName); err == nil {
|
||||
return LocalConfigName
|
||||
}
|
||||
// One level up. The server is normally started from server/ while
|
||||
// config.yaml lives at the repository root, so without this the file is
|
||||
// found only when a launcher happens to export GOAUTO_CONFIG — and a
|
||||
// developer running `go run .` by hand gets no local configuration at all.
|
||||
parent := filepath.Join("..", LocalConfigName)
|
||||
if _, err := os.Stat(parent); err == nil {
|
||||
return parent
|
||||
}
|
||||
// Next to the executable, so double-clicking a packaged binary works no
|
||||
// matter what the working directory happens to be.
|
||||
if executable, err := os.Executable(); err == nil {
|
||||
@@ -72,23 +80,23 @@ func ApplyLocalConfig() {
|
||||
// `[必须]` Say so out loud. Silently doing nothing here surfaces much
|
||||
// later as a puzzling "credential not configured" error that never
|
||||
// mentions the file the operator actually edited.
|
||||
log.Printf("本地配置:未找到 %s(依次查找 GOAUTO_CONFIG、当前目录、可执行文件同级目录),仅使用 settings.yml 和环境变量", LocalConfigName)
|
||||
logInfo("本地配置:未找到 %s(依次查找 GOAUTO_CONFIG、当前目录、上级目录、可执行文件同级目录),仅使用 settings.yml 和环境变量", LocalConfigName)
|
||||
return
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "读取本地配置 %s 失败,已忽略: %v\n", path, err)
|
||||
logInfo("本地配置:读取 %s 失败,已忽略: %v", path, err)
|
||||
return
|
||||
}
|
||||
var file localFile
|
||||
if err := yaml.Unmarshal(raw, &file); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "解析本地配置 %s 失败,已忽略: %v\n", path, err)
|
||||
logInfo("本地配置:解析 %s 失败,已忽略: %v", path, err)
|
||||
return
|
||||
}
|
||||
applyLocalDatabase(file.Database)
|
||||
applyLocalPorts(file.Ports)
|
||||
ApplyLocalSYB(file.SYB)
|
||||
log.Printf("本地配置:已加载 %s", path)
|
||||
logInfo("本地配置:已加载 %s", path)
|
||||
}
|
||||
|
||||
// LogEffectiveConfig prints what the layered configuration actually resolved
|
||||
@@ -105,7 +113,7 @@ func LogEffectiveConfig() {
|
||||
case syb.HasCredentials():
|
||||
source = LocalConfigName
|
||||
}
|
||||
log.Printf("顺云宝配置:凭据=%v(来源:%s)base_url=%s 验证码识别=%v",
|
||||
logInfo("顺云宝配置:凭据=%v(来源:%s)base_url=%s 验证码识别=%v",
|
||||
syb.HasCredentials(), source, syb.BaseURL, syb.OcrURL != "")
|
||||
}
|
||||
|
||||
@@ -169,6 +177,16 @@ func ApplyLocalSYB(syb map[string]any) {
|
||||
}
|
||||
}
|
||||
|
||||
// logInfo writes an informational startup line to stdout.
|
||||
//
|
||||
// `[必须]` Not stderr. The development launcher pipes the server through
|
||||
// `2>&1 | Tee-Object`, which turns every stderr write into a PowerShell
|
||||
// NativeCommandError — informational lines would show up as a red error block
|
||||
// and look like a failed startup.
|
||||
func logInfo(format string, args ...any) {
|
||||
fmt.Printf(time.Now().Format("2006/01/02 15:04:05")+" "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// scalar renders one YAML value as a string regardless of how YAML typed it.
|
||||
//
|
||||
// `[必须]` Floats are formatted without an exponent and without a trailing
|
||||
|
||||
@@ -218,3 +218,55 @@ func TestLocalConfigPathPrefersExplicitEnvOverCwd(t *testing.T) {
|
||||
t.Fatalf("GOAUTO_CONFIG 应优先于其它位置: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 服务端通常从 server/ 启动,而 config.yaml 在仓库根。没有这条上级目录查找,
|
||||
// 手工跑 `go run .` 就完全读不到本地配置——启动脚本导出 GOAUTO_CONFIG 只是
|
||||
// 恰好掩盖了这一点。
|
||||
func TestLocalConfigPathFindsFileInParentDirectory(t *testing.T) {
|
||||
restoreConfigState(t)
|
||||
t.Setenv("GOAUTO_CONFIG", "")
|
||||
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, LocalConfigName),
|
||||
[]byte("syb:\n username: operator\n password: \"654321\"\n"), 0o600); err != nil {
|
||||
t.Fatalf("写配置失败: %v", err)
|
||||
}
|
||||
child := filepath.Join(root, "server")
|
||||
if err := os.Mkdir(child, 0o755); err != nil {
|
||||
t.Fatalf("建目录失败: %v", err)
|
||||
}
|
||||
previous, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("取工作目录失败: %v", err)
|
||||
}
|
||||
if err := os.Chdir(child); err != nil {
|
||||
t.Fatalf("切目录失败: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { os.Chdir(previous) })
|
||||
|
||||
ApplyLocalConfig()
|
||||
if !ExtConfig.SYB.HasCredentials() {
|
||||
t.Fatal("从 server/ 启动时应能在上级目录找到 config.yaml")
|
||||
}
|
||||
}
|
||||
|
||||
// 当前目录优先于上级目录。
|
||||
func TestLocalConfigPathPrefersCwdOverParent(t *testing.T) {
|
||||
restoreConfigState(t)
|
||||
t.Setenv("GOAUTO_CONFIG", "")
|
||||
|
||||
root := t.TempDir()
|
||||
os.WriteFile(filepath.Join(root, LocalConfigName), []byte("syb:\n username: parent\n password: \"1\"\n"), 0o600)
|
||||
child := filepath.Join(root, "server")
|
||||
os.Mkdir(child, 0o755)
|
||||
os.WriteFile(filepath.Join(child, LocalConfigName), []byte("syb:\n username: cwd\n password: \"1\"\n"), 0o600)
|
||||
|
||||
previous, _ := os.Getwd()
|
||||
os.Chdir(child)
|
||||
t.Cleanup(func() { os.Chdir(previous) })
|
||||
|
||||
ApplyLocalConfig()
|
||||
if ExtConfig.SYB.Username != "cwd" {
|
||||
t.Fatalf("当前目录应优先: %q", ExtConfig.SYB.Username)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user