diff --git a/cmd/initconfig/main.go b/cmd/initconfig/main.go index 4000048..39d597b 100644 --- a/cmd/initconfig/main.go +++ b/cmd/initconfig/main.go @@ -17,6 +17,36 @@ func randomSecret(n int) string { return hex.EncodeToString(b) } +// must 让失败真正停下来。 +// +// 这里曾经把所有 db.Exec 的返回值丢掉,配合 CGO_ENABLED=0 构建(go-sqlite3 +// 退化成静态桩),得到的是一个**完全静默的空操作**:打印凭据、退出码 0、 +// config.db 里一个字节都没写。调用方(安装脚本)无法区分成败,用户装完 +// 照着 credentials.txt 登录必然失败。 +func must(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "initconfig: %v\n", err) + os.Exit(1) + } +} + +// verify 回读刚写入的值。 +// +// 只看 Exec 有没有报错不够:驱动被换掉(如上面的桩)、路径不对、写入被丢弃, +// 都可能返回 nil 而什么都没落下。这里把真实落盘的值读回来,与预期逐一比对, +// 不一致就非零退出——"初始化脚本说自己成功了"必须由数据库内容佐证。 +func verify(db *sql.DB, table, key, want string) { + var got string + if err := db.QueryRow(fmt.Sprintf(`SELECT value FROM %s WHERE key = ?`, table), key).Scan(&got); err != nil { + fmt.Fprintf(os.Stderr, "initconfig: 回读 %s.%s 失败: %v\n", table, key, err) + os.Exit(1) + } + if got != want { + fmt.Fprintf(os.Stderr, "initconfig: %s.%s 与写入值不一致(读回 %q)\n", table, key, got) + os.Exit(1) + } +} + func main() { dataDir := flag.String("data", "", "data directory") webuiUsername := flag.String("username", "admin", "webui username") @@ -33,16 +63,24 @@ func main() { dbPath := *dataDir + "/config.db" db, err := sql.Open("sqlite3", dbPath) - if err != nil { - fmt.Fprintf(os.Stderr, "open db: %v\n", err) - os.Exit(1) - } + must(err) defer db.Close() - db.Exec("PRAGMA journal_mode=WAL") + // 尽早验证数据库真的可用:sql.Open 是惰性的,不碰一次不会暴露驱动问题。 + if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { + fmt.Fprintf(os.Stderr, "initconfig: 打开数据库 %s 失败: %v\n", dbPath, err) + os.Exit(1) + } - db.Exec(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)`) - db.Exec(`INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)`, "webui.listen_addr", ":8080") + if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)`); err != nil { + fmt.Fprintf(os.Stderr, "initconfig: 创建 config 表失败: %v\n", err) + os.Exit(1) + } + const listenAddr = ":8080" + if _, err := db.Exec(`INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)`, "webui.listen_addr", listenAddr); err != nil { + fmt.Fprintf(os.Stderr, "initconfig: 写入 webui.listen_addr 失败: %v\n", err) + os.Exit(1) + } pw := *webuiPassword if pw == "" { @@ -53,13 +91,28 @@ func main() { apiKey = randomSecret(16) } - pt := "config_webui" - db.Exec(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, value TEXT NOT NULL)`, pt)) + const pt = "config_webui" + if _, err := db.Exec(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, value TEXT NOT NULL)`, pt)); err != nil { + fmt.Fprintf(os.Stderr, "initconfig: 创建 %s 表失败: %v\n", pt, err) + os.Exit(1) + } ws := fmt.Sprintf(`INSERT OR REPLACE INTO %s (key, value) VALUES (?, ?)`, pt) - db.Exec(ws, "api_key", apiKey) - db.Exec(ws, "username", *webuiUsername) - db.Exec(ws, "password", pw) - db.Exec(ws, "session_ttl_hours", "24") + for _, kv := range [][2]string{ + {"api_key", apiKey}, + {"username", *webuiUsername}, + {"password", pw}, + {"session_ttl_hours", "24"}, + } { + if _, err := db.Exec(ws, kv[0], kv[1]); err != nil { + fmt.Fprintf(os.Stderr, "initconfig: 写入 %s.%s 失败: %v\n", pt, kv[0], err) + os.Exit(1) + } + } + + verify(db, pt, "api_key", apiKey) + verify(db, pt, "username", *webuiUsername) + verify(db, pt, "password", pw) + verify(db, "config", "webui.listen_addr", listenAddr) fmt.Printf("API_KEY=%s\n", apiKey) fmt.Printf("WEBUI_USERNAME=%s\n", *webuiUsername) diff --git a/deploy/packaging/build.sh b/deploy/packaging/build.sh index 649b84f..db38a10 100644 --- a/deploy/packaging/build.sh +++ b/deploy/packaging/build.sh @@ -149,18 +149,23 @@ build_waiter() { echo " OK ($(du -h "$out" | cut -f1))" } -# ---- initconfig (CGO-free 配置初始化器) ---- +# ---- initconfig(必须 cgo:写 config.db 用的是 go-sqlite3)---- # -# NSIS 安装包(installer.nsi:220 File "..\build\initconfig.exe")与 -# package-linux.sh 的 stage_variant 都引用它,但此前 build.sh 从不构建它—— -# Windows 安装包构建会直接失败在缺文件上。 +# 这里**必须** CGO_ENABLED=1。此前写的是 CGO_ENABLED=0,而 cmd/initconfig 通过 +# database/sql 使用 mattn/go-sqlite3:CGO_ENABLED=0 时该库退化成 static_mock.go +# 里的桩,sql.Open 是懒的所以不报错、第一次 Exec 才失败;而 main.go 当时忽略 +# 了所有错误——于是 initconfig 打印凭据、退出码 0、一个字节都没写进 config.db。 +# 安装脚本把这份凭据写进 credentials.txt,用户照它登录必然失败,全程无报错。 +# +# NSIS 安装包(installer.nsi)与 package-linux.sh 的 stage_variant 都引用它, +# 但此前 build.sh 从不构建它——Windows 安装包构建会直接失败在缺文件上。 build_initconfig() { local plat="${GOOS:-linux}/${GOARCH:-amd64}" local out="$BUILD_DIR/initconfig${SUFFIX:+_$SUFFIX}" if [ "$GOOS" = "windows" ]; then out="${out}.exe"; fi echo "[BUILD] initconfig ${plat} → $out" - CGO_ENABLED=0 "$GO" build -trimpath -installsuffix dynlink \ + CGO_ENABLED=1 "$GO" build -trimpath -installsuffix dynlink \ -ldflags "$LDFLAGS" -o "$out" ./cmd/initconfig/ echo " OK ($(du -h "$out" | cut -f1))" }