mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-10 17:43:15 +08:00
🔧 fix(release,db/kingbase_impl): 修复金仓默认 schema 并静默生成 DMG
- Kingbase:在 current_schema() 为 public 时探测候选 schema,并通过 DSN search_path 重连,兼容未限定 schema 的查询 - 候选优先级:数据库名/用户名同名 schema(存在性校验),否则仅在“唯一用户 schema 有表”场景兜底 - 避免连接污染:每次 Connect 重置探测结果,重连成功后替换连接并关闭旧连接 - 打包脚本:create-dmg 增加 --sandbox-safe,避免构建时自动弹出/打开挂载窗口 - 产物格式:强制 --format UDZO,并将 rw.*.dmg/UDRW 中间产物转换为可分发 DMG - 校验门禁:增加 hdiutil verify,失败时保留 .app 便于排查,同时修正卷图标探测并补 ad-hoc 签名
This commit is contained in:
141
build-release.sh
141
build-release.sh
@@ -20,6 +20,11 @@ RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
MAC_VOLICON_PATH="build/darwin/icon.icns"
|
||||
if [ ! -f "$MAC_VOLICON_PATH" ]; then
|
||||
MAC_VOLICON_PATH=""
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}🚀 开始构建 $APP_NAME $VERSION...${NC}"
|
||||
|
||||
# 清理并创建输出目录
|
||||
@@ -37,15 +42,25 @@ if [ $? -eq 0 ]; then
|
||||
# 移动 .app 到 dist
|
||||
mv "$APP_SRC" "$DIST_DIR/$APP_DEST_NAME"
|
||||
|
||||
# Ad-hoc 代码签名(无 Apple Developer 账号时防止 Gatekeeper 报已损坏)
|
||||
echo " 🔏 正在对 .app 进行 ad-hoc 签名 (arm64)..."
|
||||
codesign --force --deep --sign - "$DIST_DIR/$APP_DEST_NAME"
|
||||
|
||||
# 创建 DMG
|
||||
if command -v create-dmg &> /dev/null; then
|
||||
echo " 📦 正在打包 DMG (arm64)..."
|
||||
# 移除已存在的 DMG (以防万一)
|
||||
rm -f "$DIST_DIR/$DMG_NAME"
|
||||
|
||||
create-dmg \
|
||||
--volname "${APP_NAME} ${VERSION}" \
|
||||
--volicon "build/appicon.icns" \
|
||||
|
||||
# --sandbox-safe 会跳过 Finder 的 AppleScript 排版,避免打包过程中弹出/打开挂载窗口(CI/本地静默打包更友好)。
|
||||
CREATE_DMG_ARGS=(--volname "${APP_NAME} ${VERSION}" --format UDZO --sandbox-safe)
|
||||
if [ -n "$MAC_VOLICON_PATH" ]; then
|
||||
CREATE_DMG_ARGS+=(--volicon "$MAC_VOLICON_PATH")
|
||||
else
|
||||
echo -e "${YELLOW} ⚠️ 未找到 macOS 卷图标 (build/darwin/icon.icns),跳过 --volicon。${NC}"
|
||||
fi
|
||||
|
||||
create-dmg "${CREATE_DMG_ARGS[@]}" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 800 400 \
|
||||
--icon-size 100 \
|
||||
@@ -54,23 +69,47 @@ if [ $? -eq 0 ]; then
|
||||
--app-drop-link 600 185 \
|
||||
"$DIST_DIR/$DMG_NAME" \
|
||||
"$DIST_DIR/$APP_DEST_NAME"
|
||||
|
||||
CREATE_DMG_EXIT_CODE=$?
|
||||
|
||||
# 检查是否生成了 rw.* 的临时文件并重命名 (create-dmg 有时会有此行为)
|
||||
if [ ! -f "$DIST_DIR/$DMG_NAME" ]; then
|
||||
RW_FILE=$(find "$DIST_DIR" -name "rw.*.dmg" -print -quit)
|
||||
if [ -n "$RW_FILE" ]; then
|
||||
echo -e "${YELLOW} ⚠️ 检测到临时文件名,正在重命名...${NC}"
|
||||
mv "$RW_FILE" "$DIST_DIR/$DMG_NAME"
|
||||
fi
|
||||
if [ $CREATE_DMG_EXIT_CODE -ne 0 ]; then
|
||||
echo -e "${RED} ❌ create-dmg 执行失败 (exit=$CREATE_DMG_EXIT_CODE),保留 .app 以便排查。${NC}"
|
||||
else
|
||||
# create-dmg 可能会在失败时遗留 rw.*.dmg 中间产物;不要直接当作最终产物使用
|
||||
if [ ! -f "$DIST_DIR/$DMG_NAME" ]; then
|
||||
RW_FILE=$(find "$DIST_DIR" -maxdepth 1 -name "rw.*.dmg" -print -quit)
|
||||
if [ -n "$RW_FILE" ]; then
|
||||
echo -e "${YELLOW} ⚠️ 检测到 create-dmg 中间产物: $(basename "$RW_FILE"),正在转换为可分发 DMG...${NC}"
|
||||
hdiutil convert "$RW_FILE" -format UDZO -o "$DIST_DIR/$DMG_NAME" >/dev/null 2>&1
|
||||
rm -f "$RW_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 防御性:即使生成了目标文件,也要确保不是 UDRW(UDRW 在 Finder 下可能表现为“已损坏/无法打开”)
|
||||
if [ -f "$DIST_DIR/$DMG_NAME" ] && command -v hdiutil &> /dev/null; then
|
||||
DMG_FORMAT=$(hdiutil imageinfo "$DIST_DIR/$DMG_NAME" 2>/dev/null | awk -F': ' '/^Format:/{print $2; exit}')
|
||||
if [ "$DMG_FORMAT" = "UDRW" ]; then
|
||||
echo -e "${YELLOW} ⚠️ 检测到 UDRW(可写原始映像),正在转换为 UDZO...${NC}"
|
||||
TMP_UDZO="$DIST_DIR/.tmp.$DMG_NAME"
|
||||
rm -f "$TMP_UDZO"
|
||||
hdiutil convert "$DIST_DIR/$DMG_NAME" -format UDZO -o "$TMP_UDZO" >/dev/null 2>&1 && mv "$TMP_UDZO" "$DIST_DIR/$DMG_NAME"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$DIST_DIR/$DMG_NAME" ] && command -v hdiutil &> /dev/null; then
|
||||
hdiutil verify "$DIST_DIR/$DMG_NAME" >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED} ❌ DMG 校验失败,保留 .app 以便排查。${NC}"
|
||||
else
|
||||
# 删除中间的 .app 文件,保持目录整洁
|
||||
rm -rf "$DIST_DIR/$APP_DEST_NAME"
|
||||
echo " ✅ 已生成 $DMG_NAME"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 删除中间的 .app 文件,保持目录整洁
|
||||
rm -rf "$DIST_DIR/$APP_DEST_NAME"
|
||||
|
||||
if [ -f "$DIST_DIR/$DMG_NAME" ]; then
|
||||
echo " ✅ 已生成 $DMG_NAME"
|
||||
else
|
||||
echo -e "${RED} ❌ DMG 生成失败,请检查 create-dmg 输出。${NC}"
|
||||
if [ ! -f "$DIST_DIR/$DMG_NAME" ]; then
|
||||
echo -e "${RED} ❌ DMG 生成失败,请检查 create-dmg 输出。${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW} ⚠️ 未找到 create-dmg 工具,跳过 DMG 打包,仅保留 .app。${NC}"
|
||||
@@ -90,13 +129,23 @@ if [ $? -eq 0 ]; then
|
||||
|
||||
mv "$APP_SRC" "$DIST_DIR/$APP_DEST_NAME"
|
||||
|
||||
# Ad-hoc 代码签名
|
||||
echo " 🔏 正在对 .app 进行 ad-hoc 签名 (amd64)..."
|
||||
codesign --force --deep --sign - "$DIST_DIR/$APP_DEST_NAME"
|
||||
|
||||
if command -v create-dmg &> /dev/null; then
|
||||
echo " 📦 正在打包 DMG (amd64)..."
|
||||
rm -f "$DIST_DIR/$DMG_NAME"
|
||||
|
||||
create-dmg \
|
||||
--volname "${APP_NAME} ${VERSION}" \
|
||||
--volicon "build/appicon.icns" \
|
||||
|
||||
# --sandbox-safe 会跳过 Finder 的 AppleScript 排版,避免打包过程中弹出/打开挂载窗口(CI/本地静默打包更友好)。
|
||||
CREATE_DMG_ARGS=(--volname "${APP_NAME} ${VERSION}" --format UDZO --sandbox-safe)
|
||||
if [ -n "$MAC_VOLICON_PATH" ]; then
|
||||
CREATE_DMG_ARGS+=(--volicon "$MAC_VOLICON_PATH")
|
||||
else
|
||||
echo -e "${YELLOW} ⚠️ 未找到 macOS 卷图标 (build/darwin/icon.icns),跳过 --volicon。${NC}"
|
||||
fi
|
||||
|
||||
create-dmg "${CREATE_DMG_ARGS[@]}" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 800 400 \
|
||||
--icon-size 100 \
|
||||
@@ -106,21 +155,43 @@ if [ $? -eq 0 ]; then
|
||||
"$DIST_DIR/$DMG_NAME" \
|
||||
"$DIST_DIR/$APP_DEST_NAME"
|
||||
|
||||
# 检查是否生成了 rw.* 的临时文件并重命名
|
||||
if [ ! -f "$DIST_DIR/$DMG_NAME" ]; then
|
||||
RW_FILE=$(find "$DIST_DIR" -name "rw.*.dmg" -print -quit)
|
||||
if [ -n "$RW_FILE" ]; then
|
||||
echo -e "${YELLOW} ⚠️ 检测到临时文件名,正在重命名...${NC}"
|
||||
mv "$RW_FILE" "$DIST_DIR/$DMG_NAME"
|
||||
fi
|
||||
CREATE_DMG_EXIT_CODE=$?
|
||||
|
||||
if [ $CREATE_DMG_EXIT_CODE -ne 0 ]; then
|
||||
echo -e "${RED} ❌ create-dmg 执行失败 (exit=$CREATE_DMG_EXIT_CODE),保留 .app 以便排查。${NC}"
|
||||
else
|
||||
if [ ! -f "$DIST_DIR/$DMG_NAME" ]; then
|
||||
RW_FILE=$(find "$DIST_DIR" -maxdepth 1 -name "rw.*.dmg" -print -quit)
|
||||
if [ -n "$RW_FILE" ]; then
|
||||
echo -e "${YELLOW} ⚠️ 检测到 create-dmg 中间产物: $(basename "$RW_FILE"),正在转换为可分发 DMG...${NC}"
|
||||
hdiutil convert "$RW_FILE" -format UDZO -o "$DIST_DIR/$DMG_NAME" >/dev/null 2>&1
|
||||
rm -f "$RW_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$DIST_DIR/$DMG_NAME" ] && command -v hdiutil &> /dev/null; then
|
||||
DMG_FORMAT=$(hdiutil imageinfo "$DIST_DIR/$DMG_NAME" 2>/dev/null | awk -F': ' '/^Format:/{print $2; exit}')
|
||||
if [ "$DMG_FORMAT" = "UDRW" ]; then
|
||||
echo -e "${YELLOW} ⚠️ 检测到 UDRW(可写原始映像),正在转换为 UDZO...${NC}"
|
||||
TMP_UDZO="$DIST_DIR/.tmp.$DMG_NAME"
|
||||
rm -f "$TMP_UDZO"
|
||||
hdiutil convert "$DIST_DIR/$DMG_NAME" -format UDZO -o "$TMP_UDZO" >/dev/null 2>&1 && mv "$TMP_UDZO" "$DIST_DIR/$DMG_NAME"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$DIST_DIR/$DMG_NAME" ] && command -v hdiutil &> /dev/null; then
|
||||
hdiutil verify "$DIST_DIR/$DMG_NAME" >/dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED} ❌ DMG 校验失败,保留 .app 以便排查。${NC}"
|
||||
else
|
||||
rm -rf "$DIST_DIR/$APP_DEST_NAME"
|
||||
echo " ✅ 已生成 $DMG_NAME"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -rf "$DIST_DIR/$APP_DEST_NAME"
|
||||
|
||||
if [ -f "$DIST_DIR/$DMG_NAME" ]; then
|
||||
echo " ✅ 已生成 $DMG_NAME"
|
||||
else
|
||||
echo -e "${RED} ❌ DMG 生成失败。${NC}"
|
||||
if [ ! -f "$DIST_DIR/$DMG_NAME" ]; then
|
||||
echo -e "${RED} ❌ DMG 生成失败。${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW} ⚠️ 未找到 create-dmg 工具。${NC}"
|
||||
|
||||
@@ -21,9 +21,10 @@ import (
|
||||
)
|
||||
|
||||
type KingbaseDB struct {
|
||||
conn *sql.DB
|
||||
pingTimeout time.Duration
|
||||
forwarder *ssh.LocalForwarder // Store SSH tunnel forwarder
|
||||
conn *sql.DB
|
||||
pingTimeout time.Duration
|
||||
defaultSearchPath string
|
||||
forwarder *ssh.LocalForwarder // Store SSH tunnel forwarder
|
||||
}
|
||||
|
||||
func quoteConnValue(v string) string {
|
||||
@@ -75,6 +76,9 @@ func (k *KingbaseDB) getDSN(config connection.ConnectionConfig) string {
|
||||
quoteConnValue(resolvePostgresSSLMode(config)),
|
||||
getConnectTimeoutSeconds(config),
|
||||
)
|
||||
if strings.TrimSpace(k.defaultSearchPath) != "" {
|
||||
dsn += fmt.Sprintf(" search_path=%s", quoteConnValue(k.defaultSearchPath))
|
||||
}
|
||||
|
||||
return dsn
|
||||
}
|
||||
@@ -120,6 +124,9 @@ func (k *KingbaseDB) Connect(config connection.ConnectionConfig) error {
|
||||
|
||||
var failures []string
|
||||
for idx, attempt := range attempts {
|
||||
// 避免跨连接缓存 defaultSearchPath 造成的污染:每次 Connect 都重新探测一次。
|
||||
k.defaultSearchPath = ""
|
||||
|
||||
dsn := k.getDSN(attempt)
|
||||
db, err := sql.Open("kingbase", dsn)
|
||||
if err != nil {
|
||||
@@ -137,11 +144,166 @@ func (k *KingbaseDB) Connect(config connection.ConnectionConfig) error {
|
||||
if idx > 0 {
|
||||
logger.Warnf("人大金仓 SSL 优先连接失败,已回退至明文连接")
|
||||
}
|
||||
|
||||
k.reconnectWithPreferredSearchPathIfNeeded(attempt)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("连接建立后验证失败:%s", strings.Join(failures, ";"))
|
||||
}
|
||||
|
||||
func (k *KingbaseDB) reconnectWithPreferredSearchPathIfNeeded(config connection.ConnectionConfig) {
|
||||
if k.conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
timeout := k.pingTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
ctx, cancel := utils.ContextWithTimeout(timeout)
|
||||
defer cancel()
|
||||
|
||||
var currentSchema string
|
||||
if err := k.conn.QueryRowContext(ctx, "SELECT current_schema()").Scan(¤tSchema); err != nil {
|
||||
logger.Warnf("人大金仓读取当前 schema 失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if schema := strings.TrimSpace(currentSchema); schema != "" && !strings.EqualFold(schema, "public") {
|
||||
return
|
||||
}
|
||||
|
||||
searchPath, chosenSchema := k.detectPreferredSearchPath(ctx, config)
|
||||
if strings.TrimSpace(searchPath) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
oldConn := k.conn
|
||||
prevSearchPath := k.defaultSearchPath
|
||||
k.defaultSearchPath = searchPath
|
||||
|
||||
dsn := k.getDSN(config)
|
||||
newConn, err := sql.Open("kingbase", dsn)
|
||||
if err != nil {
|
||||
k.defaultSearchPath = prevSearchPath
|
||||
logger.Warnf("人大金仓重连以设置 search_path 失败:%v", err)
|
||||
return
|
||||
}
|
||||
if err := newConn.PingContext(ctx); err != nil {
|
||||
_ = newConn.Close()
|
||||
k.defaultSearchPath = prevSearchPath
|
||||
logger.Warnf("人大金仓重连后验证失败:%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
k.conn = newConn
|
||||
_ = oldConn.Close()
|
||||
logger.Infof("人大金仓已设置默认 schema:%s", chosenSchema)
|
||||
}
|
||||
|
||||
func (k *KingbaseDB) kingbaseSchemaExists(ctx context.Context, schema string) (bool, error) {
|
||||
if schema = strings.TrimSpace(schema); schema == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var one int
|
||||
err := k.conn.QueryRowContext(ctx, "SELECT 1 FROM pg_namespace WHERE nspname = $1", schema).Scan(&one)
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (k *KingbaseDB) detectPreferredSearchPath(ctx context.Context, config connection.ConnectionConfig) (searchPath string, chosenSchema string) {
|
||||
// 1) 优先使用与数据库名/用户名同名的 schema(需要存在)
|
||||
candidates := []string{
|
||||
normalizeKingbaseIdentifier(config.Database),
|
||||
normalizeKingbaseIdentifier(config.User),
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{}, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
if candidate == "" || strings.EqualFold(candidate, "public") {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(candidate)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
|
||||
exists, err := k.kingbaseSchemaExists(ctx, candidate)
|
||||
if err != nil {
|
||||
logger.Warnf("人大金仓检查 schema 是否存在失败:schema=%s err=%v", candidate, err)
|
||||
continue
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s,public", quoteKingbaseIdent(candidate)), candidate
|
||||
}
|
||||
|
||||
// 2) 如果只有一个“用户 schema”含有表,则将其作为默认 schema(更符合 DB GUI 的直觉)
|
||||
schema, err := k.detectSingleUserSchemaWithTables(ctx)
|
||||
if err != nil {
|
||||
logger.Warnf("人大金仓探测默认 schema 失败:%v", err)
|
||||
return "", ""
|
||||
}
|
||||
if schema == "" || strings.EqualFold(schema, "public") {
|
||||
return "", ""
|
||||
}
|
||||
return fmt.Sprintf("%s,public", quoteKingbaseIdent(schema)), schema
|
||||
}
|
||||
|
||||
func (k *KingbaseDB) detectSingleUserSchemaWithTables(ctx context.Context) (string, error) {
|
||||
if k.conn == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 仅在“唯一用户 schema”场景做兜底,避免多 schema 下误选导致对象解析歧义。
|
||||
// 注:information_schema.tables 的视图在 PG/金仓语义稳定且权限要求相对低。
|
||||
query := `
|
||||
SELECT table_schema, COUNT(*) AS table_count
|
||||
FROM information_schema.tables
|
||||
WHERE table_type = 'BASE TABLE'
|
||||
AND table_schema NOT IN ('pg_catalog', 'information_schema', 'public')
|
||||
AND table_schema NOT LIKE 'pg_%'
|
||||
GROUP BY table_schema
|
||||
ORDER BY table_count DESC, table_schema
|
||||
LIMIT 2`
|
||||
|
||||
rows, err := k.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type row struct {
|
||||
schema string
|
||||
count int64
|
||||
}
|
||||
var results []row
|
||||
for rows.Next() {
|
||||
var r row
|
||||
if scanErr := rows.Scan(&r.schema, &r.count); scanErr != nil {
|
||||
return "", scanErr
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(results) != 1 {
|
||||
return "", nil
|
||||
}
|
||||
return normalizeKingbaseIdentifier(results[0].schema), nil
|
||||
}
|
||||
|
||||
func (k *KingbaseDB) Close() error {
|
||||
// Close SSH forwarder first if exists
|
||||
if k.forwarder != nil {
|
||||
|
||||
Reference in New Issue
Block a user