From 82bb54e97c2e8707c455ddee2f9c7b8e09bffbd0 Mon Sep 17 00:00:00 2001 From: limitcool Date: Wed, 1 Apr 2026 13:57:21 +0800 Subject: [PATCH 1/2] fix: preserve runtime DB config for reposync subprocess --- internal/constant/constant.go | 16 ++++++- internal/services/config_service.go | 11 ++++- internal/services/tasks/executor_service.go | 46 ++++++++++++++++----- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/internal/constant/constant.go b/internal/constant/constant.go index f305418..8f07961 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -123,7 +123,7 @@ const ( // Agent 状态 AgentStatusOnline = "online" AgentStatusOffline = "offline" - + // AppLog 分类 LogCategoryDefault = "default" LogCategorySystemNotice = "system_notice" @@ -149,6 +149,20 @@ const ( // TablePrefix 表前缀,从配置文件读取 var TablePrefix string +// Runtime 数据库配置快照,用于需要单独启动内部子进程(如 reposync)时显式透传数据库连接信息, +// 避免主进程启动阶段清理环境变量后,子进程意外回退到默认 sqlite 配置。 +var ( + RuntimeDBType string + RuntimeDBHost string + RuntimeDBPort int + RuntimeDBUser string + RuntimeDBPassword string + RuntimeDBName string + RuntimeDBPath string + RuntimeDBDSN string + RuntimeDBTablePrefix string +) + // Secret JWT和密码salt密钥,运行中自动从数据库加载 var Secret string diff --git a/internal/services/config_service.go b/internal/services/config_service.go index 1eb40d2..8ffe990 100644 --- a/internal/services/config_service.go +++ b/internal/services/config_service.go @@ -11,7 +11,7 @@ import ( ) type ServerConfig struct { - Port int `ini:"port"` + Port int `ini:"port"` Host string `ini:"host"` URLPrefix string `ini:"url_prefix"` PprofEnabled bool `ini:"pprof_enabled"` @@ -125,6 +125,15 @@ func LoadConfig(path string) (*AppConfig, error) { // 设置表前缀到 constant 包 constant.TablePrefix = Config.Database.TablePrefix + constant.RuntimeDBType = Config.Database.Type + constant.RuntimeDBHost = Config.Database.Host + constant.RuntimeDBPort = Config.Database.Port + constant.RuntimeDBUser = Config.Database.User + constant.RuntimeDBPassword = Config.Database.Password + constant.RuntimeDBName = Config.Database.DBName + constant.RuntimeDBPath = Config.Database.Path + constant.RuntimeDBDSN = Config.Database.DSN + constant.RuntimeDBTablePrefix = Config.Database.TablePrefix // 暂存旧的 Secret,不再直接给 constant 赋值(改为到 settings 初始化时判断) // constant.Secret = Config.Security.Secret diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index 98411ae..5b796e8 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -243,8 +243,6 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest, // 处理任务完成(更新统计、清理旧日志等) h.es.taskLogService.ProcessTaskCompletion(taskLog) - - // 更新内存缓冲 h.es.UpdateResult(*result) @@ -362,14 +360,14 @@ func (es *ExecutorService) HandleTaskRetry(task *models.Task, req *executor.Exec if task == nil { return } - + if !isSuccess || status == constant.TaskStatusFailed || status == constant.TaskStatusTimeout || exitCode != 0 { retryIndex := req.Metadata.RetryIndex if retryIndex < task.RetryCount { retryIndex++ logger.Infof("[Executor] 任务 #%s 执行失败/出错,将在 %d 秒后进行第 %d/%d 次重试...", task.ID, task.RetryInterval, retryIndex, task.RetryCount) - + es.scheduler.EnqueueDelayed(time.Duration(task.RetryInterval)*time.Second, func() *executor.ExecutionRequest { latestTask := es.taskService.GetTaskByID(task.ID) if latestTask == nil || !latestTask.Enabled { @@ -427,7 +425,7 @@ func (h *LocalTaskHooks) OnHeartbeat(ctx context.Context, logID string, duration // ExecuteDispatcher 实现任务分发逻辑 func (es *ExecutorService) ExecuteDispatcher(ctx context.Context, req *executor.ExecutionRequest, stdout, stderr io.Writer) (*executor.Result, error) { taskID := req.TaskID - + // 解析路径变量 (如 $SCRIPTS_DIR$) req.Command = es.ResolvePath(req.Command) req.WorkDir = es.ResolvePath(req.WorkDir) @@ -770,8 +768,8 @@ func (es *ExecutorService) CheckConcurrency(taskID string) error { if string(task.RunningGo) != "" { _ = json.Unmarshal([]byte(string(task.RunningGo)), &goids) } - - var config models.TaskConfig + + var config models.TaskConfig if string(task.Config) != "" { _ = json.Unmarshal([]byte(string(task.Config)), &config) } @@ -990,7 +988,7 @@ func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string) if config.Extensions != "" { args = append(args, "--extensions", config.Extensions) } - + // 传递任务 ID,以便 reposync 内部直接处理脚本注册并输出日志 args = append(args, "--task-id", task.ID) args = append(args, "--task-timeout", fmt.Sprintf("%d", task.Timeout)) @@ -1005,7 +1003,7 @@ func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string) } cmdStr := utils.QuotePath(exePath) + " " + strings.Join(quotedArgs, " ") - return buildRepoCommandEnvPrefix()+cmdStr, filepath.Dir(exePath) + return buildRepoCommandEnvPrefix() + cmdStr, filepath.Dir(exePath) } // loadEnvVars 加载环境变量和掩码信息,支持全局注入及重名合并 @@ -1083,7 +1081,35 @@ func buildRepoCommandEnvPrefix() string { } absScriptsDir := resolveAbsScriptsDir() - return "BH_CONFIG_PATH='" + strings.ReplaceAll(absConfig, "'", "'\\''") + "' BH_SCRIPTS_DIR='" + strings.ReplaceAll(absScriptsDir, "'", "'\\''") + "' " + parts := []string{ + shellEnvAssignment("BH_CONFIG_PATH", absConfig), + shellEnvAssignment("BH_SCRIPTS_DIR", absScriptsDir), + } + + appendIfSet := func(key string, value string) { + if strings.TrimSpace(value) == "" { + return + } + parts = append(parts, shellEnvAssignment(key, value)) + } + + appendIfSet("BH_DB_TYPE", constant.RuntimeDBType) + appendIfSet("BH_DB_HOST", constant.RuntimeDBHost) + if constant.RuntimeDBPort > 0 { + parts = append(parts, shellEnvAssignment("BH_DB_PORT", fmt.Sprintf("%d", constant.RuntimeDBPort))) + } + appendIfSet("BH_DB_USER", constant.RuntimeDBUser) + appendIfSet("BH_DB_PASSWORD", constant.RuntimeDBPassword) + appendIfSet("BH_DB_NAME", constant.RuntimeDBName) + appendIfSet("BH_DB_PATH", constant.RuntimeDBPath) + appendIfSet("BH_DB_DSN", constant.RuntimeDBDSN) + appendIfSet("BH_DB_TABLE_PREFIX", constant.RuntimeDBTablePrefix) + + return strings.Join(parts, " ") + " " +} + +func shellEnvAssignment(key, value string) string { + return key + "='" + strings.ReplaceAll(value, "'", "'\\''") + "'" } func resolveAbsScriptsDir() string { From ea1d9f2306042be93df334df02bf442481ec4dfc Mon Sep 17 00:00:00 2001 From: limitcool Date: Wed, 1 Apr 2026 16:12:29 +0800 Subject: [PATCH 2/2] fix: share runtime env injection for reposync and terminal --- internal/controllers/terminal_controller.go | 53 +++++----- internal/services/tasks/executor_service.go | 63 +----------- internal/utils/runtime_env.go | 104 ++++++++++++++++++++ 3 files changed, 135 insertions(+), 85 deletions(-) create mode 100644 internal/utils/runtime_env.go diff --git a/internal/controllers/terminal_controller.go b/internal/controllers/terminal_controller.go index f961237..5f12de4 100644 --- a/internal/controllers/terminal_controller.go +++ b/internal/controllers/terminal_controller.go @@ -1,8 +1,8 @@ package controllers import ( - "encoding/json" "bufio" + "encoding/json" "io" "net/http" "os" @@ -89,7 +89,6 @@ func (tc *TerminalController) HandleWebSocket(c *gin.Context) { if userID == "" { userID = "1" // 兜底 } - if runtime.GOOS == "windows" { tc.handlePipeMode(conn, userID) } else { @@ -108,17 +107,7 @@ func (tc *TerminalController) handlePtyMode(conn *websocket.Conn, userID string) cmd.Dir = absDir } - cmd.Env = append(os.Environ(), "TERM=xterm-256color") - - // 注入 baihu 命令环境变量 - if absBinDir, err := filepath.Abs(filepath.Join(constant.DataDir, "bin")); err == nil { - pathStr := absBinDir + string(os.PathListSeparator) + os.Getenv("PATH") - cmd.Env = append(cmd.Env, "PATH="+pathStr) - } - - // 注入环境变量(支持同名合并) - envVars := tc.envService.GetFormattedEnvVarsByUserID(userID) - cmd.Env = append(cmd.Env, envVars...) + cmd.Env = tc.buildTerminalEnv(userID, "TERM=xterm-256color") ptmx, err := pty.Start(cmd) if err != nil { @@ -195,17 +184,7 @@ func (tc *TerminalController) handlePipeMode(conn *websocket.Conn, userID string } // 注入环境变量 - cmd.Env = os.Environ() - - // 注入 baihu 命令环境变量 - if absBinDir, err := filepath.Abs(filepath.Join(constant.DataDir, "bin")); err == nil { - pathStr := absBinDir + string(os.PathListSeparator) + os.Getenv("PATH") - cmd.Env = append(cmd.Env, "PATH="+pathStr) - } - - // 注入环境变量(支持同名合并) - envVars := tc.envService.GetFormattedEnvVarsByUserID(userID) - cmd.Env = append(cmd.Env, envVars...) + cmd.Env = tc.buildTerminalEnv(userID) stdin, err := cmd.StdinPipe() if err != nil { @@ -303,6 +282,11 @@ func (tc *TerminalController) ExecuteShellCommand(c *gin.Context) { } cmd := utils.NewShellCommandCmd(req.Command) + userID := c.GetString("userID") + if userID == "" { + userID = "1" // 与 WebSocket 终端保持一致,保留原有兜底行为 + } + cmd.Env = tc.buildTerminalEnv(userID) output, err := cmd.CombinedOutput() if err != nil { @@ -318,6 +302,27 @@ func (tc *TerminalController) ExecuteShellCommand(c *gin.Context) { }) } +func (tc *TerminalController) buildTerminalEnv(userID string, extraEnvs ...string) []string { + env := os.Environ() + env = append(env, extraEnvs...) + + // 注入 baihu 命令运行时路径与配置,保证在终端里手动执行 baihu 子命令时 + // 仍然能连接到与主服务一致的数据库,而不是回退到默认 sqlite。 + if absBinDir, err := filepath.Abs(filepath.Join(constant.DataDir, "bin")); err == nil { + pathStr := absBinDir + string(os.PathListSeparator) + os.Getenv("PATH") + env = append(env, "PATH="+pathStr) + } + env = append(env, utils.BuildRuntimeProcessEnv()...) + + // 注入环境变量(支持同名合并) + if tc.envService != nil { + envVars := tc.envService.GetFormattedEnvVarsByUserID(userID) + env = append(env, envVars...) + } + + return env +} + // GetCommands 获取所有可用的 cmd 列表及说明 func (tc *TerminalController) GetCommands(c *gin.Context) { var cmds []map[string]string diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index 5b796e8..85860aa 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -1075,68 +1075,9 @@ func (es *ExecutorService) ResolvePath(path string) string { } func buildRepoCommandEnvPrefix() string { - absConfig, err := filepath.Abs(constant.ConfigPath) - if err != nil { - absConfig = constant.ConfigPath - } - - absScriptsDir := resolveAbsScriptsDir() - parts := []string{ - shellEnvAssignment("BH_CONFIG_PATH", absConfig), - shellEnvAssignment("BH_SCRIPTS_DIR", absScriptsDir), - } - - appendIfSet := func(key string, value string) { - if strings.TrimSpace(value) == "" { - return - } - parts = append(parts, shellEnvAssignment(key, value)) - } - - appendIfSet("BH_DB_TYPE", constant.RuntimeDBType) - appendIfSet("BH_DB_HOST", constant.RuntimeDBHost) - if constant.RuntimeDBPort > 0 { - parts = append(parts, shellEnvAssignment("BH_DB_PORT", fmt.Sprintf("%d", constant.RuntimeDBPort))) - } - appendIfSet("BH_DB_USER", constant.RuntimeDBUser) - appendIfSet("BH_DB_PASSWORD", constant.RuntimeDBPassword) - appendIfSet("BH_DB_NAME", constant.RuntimeDBName) - appendIfSet("BH_DB_PATH", constant.RuntimeDBPath) - appendIfSet("BH_DB_DSN", constant.RuntimeDBDSN) - appendIfSet("BH_DB_TABLE_PREFIX", constant.RuntimeDBTablePrefix) - - return strings.Join(parts, " ") + " " -} - -func shellEnvAssignment(key, value string) string { - return key + "='" + strings.ReplaceAll(value, "'", "'\\''") + "'" + return utils.BuildShellEnvPrefix(utils.BuildRuntimeProcessEnv()) } func resolveAbsScriptsDir() string { - if scriptsDir := os.Getenv("BH_SCRIPTS_DIR"); scriptsDir != "" { - if filepath.IsAbs(scriptsDir) { - return filepath.Clean(scriptsDir) - } - if absScriptsDir, err := filepath.Abs(scriptsDir); err == nil { - return absScriptsDir - } - return filepath.Clean(scriptsDir) - } - - if configPath := os.Getenv("BH_CONFIG_PATH"); configPath != "" { - if !filepath.IsAbs(configPath) { - if absConfigPath, err := filepath.Abs(configPath); err == nil { - configPath = absConfigPath - } - } - - projectRoot := filepath.Dir(filepath.Dir(configPath)) - return filepath.Clean(filepath.Join(projectRoot, constant.ScriptsWorkDir)) - } - - if absScriptsDir, err := filepath.Abs(constant.ScriptsWorkDir); err == nil { - return absScriptsDir - } - - return filepath.Clean(constant.ScriptsWorkDir) + return utils.ResolveAbsScriptsDir() } diff --git a/internal/utils/runtime_env.go b/internal/utils/runtime_env.go new file mode 100644 index 0000000..4d7672e --- /dev/null +++ b/internal/utils/runtime_env.go @@ -0,0 +1,104 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/engigu/baihu-panel/internal/constant" +) + +// BuildRuntimeProcessEnv 构造 Baihu 内部可信子进程需要继承的运行时环境变量。 +// 仅包含 Baihu 自己的路径/数据库配置,不包含用户任务环境变量。 +func BuildRuntimeProcessEnv() []string { + envs := make([]string, 0, 11) + + configPath := constant.ConfigPath + if absConfig, err := filepath.Abs(constant.ConfigPath); err == nil { + configPath = absConfig + } + envs = append(envs, formatEnvVar("BH_CONFIG_PATH", configPath)) + + if scriptsDir := ResolveAbsScriptsDir(); strings.TrimSpace(scriptsDir) != "" { + envs = append(envs, formatEnvVar("BH_SCRIPTS_DIR", scriptsDir)) + } + + appendEnvIfSet(&envs, "BH_DB_TYPE", constant.RuntimeDBType) + appendEnvIfSet(&envs, "BH_DB_HOST", constant.RuntimeDBHost) + if constant.RuntimeDBPort > 0 { + envs = append(envs, formatEnvVar("BH_DB_PORT", fmt.Sprintf("%d", constant.RuntimeDBPort))) + } + appendEnvIfSet(&envs, "BH_DB_USER", constant.RuntimeDBUser) + appendEnvIfSet(&envs, "BH_DB_PASSWORD", constant.RuntimeDBPassword) + appendEnvIfSet(&envs, "BH_DB_NAME", constant.RuntimeDBName) + appendEnvIfSet(&envs, "BH_DB_PATH", constant.RuntimeDBPath) + appendEnvIfSet(&envs, "BH_DB_DSN", constant.RuntimeDBDSN) + appendEnvIfSet(&envs, "BH_DB_TABLE_PREFIX", constant.RuntimeDBTablePrefix) + + return envs +} + +// BuildShellEnvPrefix 将 KEY=VALUE 环境变量切片转换为 shell 前缀。 +func BuildShellEnvPrefix(envs []string) string { + parts := make([]string, 0, len(envs)) + for _, env := range envs { + key, value, ok := strings.Cut(env, "=") + if !ok || strings.TrimSpace(key) == "" { + continue + } + parts = append(parts, ShellEnvAssignment(key, value)) + } + + if len(parts) == 0 { + return "" + } + + return strings.Join(parts, " ") + " " +} + +// ShellEnvAssignment 生成 shell 可安全使用的 KEY='VALUE' 赋值片段。 +func ShellEnvAssignment(key, value string) string { + return key + "='" + strings.ReplaceAll(value, "'", "'\\''") + "'" +} + +// ResolveAbsScriptsDir 解析 Baihu 运行时脚本目录的绝对路径。 +func ResolveAbsScriptsDir() string { + if scriptsDir := os.Getenv("BH_SCRIPTS_DIR"); scriptsDir != "" { + if filepath.IsAbs(scriptsDir) { + return filepath.Clean(scriptsDir) + } + if absScriptsDir, err := filepath.Abs(scriptsDir); err == nil { + return absScriptsDir + } + return filepath.Clean(scriptsDir) + } + + if configPath := os.Getenv("BH_CONFIG_PATH"); configPath != "" { + if !filepath.IsAbs(configPath) { + if absConfigPath, err := filepath.Abs(configPath); err == nil { + configPath = absConfigPath + } + } + + projectRoot := filepath.Dir(filepath.Dir(configPath)) + return filepath.Clean(filepath.Join(projectRoot, constant.ScriptsWorkDir)) + } + + if absScriptsDir, err := filepath.Abs(constant.ScriptsWorkDir); err == nil { + return absScriptsDir + } + + return filepath.Clean(constant.ScriptsWorkDir) +} + +func appendEnvIfSet(envs *[]string, key, value string) { + if strings.TrimSpace(value) == "" { + return + } + *envs = append(*envs, formatEnvVar(key, value)) +} + +func formatEnvVar(key, value string) string { + return fmt.Sprintf("%s=%s", key, value) +}