Merge pull request #74 from limitcool/main
fix: preserve runtime DB config for reposync subprocess
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 加载环境变量和掩码信息,支持全局注入及重名合并
|
||||
@@ -1077,40 +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()
|
||||
return "BH_CONFIG_PATH='" + strings.ReplaceAll(absConfig, "'", "'\\''") + "' BH_SCRIPTS_DIR='" + strings.ReplaceAll(absScriptsDir, "'", "'\\''") + "' "
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user