package script import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "time" "github.com/infraboard/mcube/v2/ioc" ) const ( APP_NAME = "script_excutor" ) // ExecuteScript 执行脚本的接口函数 // 当前这个脚本引擎对象,别托管到ioc里了,直接暴露一个全局函数就好了, // 毕竟这个函数是我们对外提供的接口,没必要让调用者关心它是怎么实现的 func ExecuteScript(ctx context.Context, in *ExecuteScriptRequest) (*ExecutionResult, error) { // 从IOC容器中获取ScriptExcutor实例 executor := ioc.Controller().Get(APP_NAME).(*ScriptExcutor) return executor.ExecuteScript(ctx, in) } func NewExecuteScriptRequest(scriptPath string, args []string) *ExecuteScriptRequest { return &ExecuteScriptRequest{ ScriptPath: scriptPath, Args: args, envVars: make(map[string]string), createProcessGroup: true, // 默认启用进程组管理 useProcessGroupKill: true, // 默认使用进程组杀死方式 } } // ExecuteScriptRequest 定义了执行脚本所需的参数和配置 type ExecuteScriptRequest struct { ScriptPath string Args []string Assets []string // 资产目录列表 // 脚本工作目录(默认当前目录) workDir string // 脚本执行环境变量(默认空) envVars map[string]string // 脚本执行日志文件路径(默认空,表示不记录日志) logFile string // 脚本执行超时时间(默认 0,表示不超时) timeout time.Duration // 脚本执行的命令元数据(可选),用于日志记录和监控 metadata *CommandMetadata // 脚本执行结果文件路径(默认空,表示不保存结果) resultFile string // 需要收集内容的文件列表 collectFiles []string // 日志回调函数, 用于实时输出日志(默认 nil,表示不使用回调) logCallback func(string) // 进程组管理控制参数, 用于避免脚本执行过程中,产生的子进程无法被正确杀死的问题 // 是否创建新的进程组(默认 true,用于杀死子进程树) // 设为 false 时,子进程不会被放入新的进程组,不能被组杀 createProcessGroup bool // 是否自定义 Cancel 函数以杀死进程组(默认 true) // 设为 false 时,使用默认的进程杀死方式 useProcessGroupKill bool // 命令执行信息 cmd *exec.Cmd } func (s *ExecuteScriptRequest) SetWorkDir(dir string) { s.workDir = dir if s.metadata != nil { s.metadata.WorkDir = dir } } // GetResultFilePath 获取结果文件路径 func (s *ExecuteScriptRequest) GetResultFilePath() string { if filepath.IsAbs(s.resultFile) { return s.resultFile } return filepath.Join(s.workDir, s.resultFile) } // SetTimeout 设置脚本执行超时时间 func (s *ExecuteScriptRequest) SetTimeout(timeout time.Duration) { s.timeout = timeout if s.metadata != nil { s.metadata.Timeout = timeout } } // SetLogFile 设置脚本执行日志文件路径 func (s *ExecuteScriptRequest) SetLogFile(path string) { s.logFile = path } func (s *ExecuteScriptRequest) SetLogCallback(callback func(string)) { s.logCallback = callback } // SetEnv 设置环境变量, key会被强制转换为大写 func (s *ExecuteScriptRequest) SetEnv(key, value string) { key = strings.ToUpper(strings.TrimSpace(key)) s.envVars[key] = value if s.metadata != nil { s.metadata.EnvVars[key] = value } } // SetDebugScript 设置是否启用调试脚本 // 通过环境变量 DEBUG_SCRIPT 控制 (值为 true 或 1 时启用) func (s *ExecuteScriptRequest) SetDebugScript(v bool) { if v { s.envVars["DEBUG_SCRIPT"] = "true" } else { s.envVars["DEBUG_SCRIPT"] = "false" } } // buildEnv 构建环境变量, 将请求中的环境变量与系统环境变量合并,返回一个新的环境变量列表 func (s *ExecuteScriptRequest) buildEnv() []string { env := os.Environ() // 获取系统环境变量 // 补充自定义环境变量 for key, value := range s.envVars { env = append(env, fmt.Sprintf("%s=%s", key, value)) } return env } // isDebugScriptEnabled 检查是否启用调试脚本 // 通过环境变量 DEBUG_SCRIPT 控制 (值为 true 或 1 时启用) func (s *ExecuteScriptRequest) isDebugScriptEnabled() bool { if val, ok := s.envVars["DEBUG_SCRIPT"]; ok { return strings.EqualFold(val, "true") || val == "1" } return false } func (r *ExecuteScriptRequest) WithWorkspacePrefix(workDirPrefix string) *ExecuteScriptRequest { if strings.HasPrefix(r.workDir, workDirPrefix) { return r } r.workDir = workDirPrefix + "/" + r.workDir return r } // getLogWriter 获取日志文件写入器 // 如果指定了日志回调函数,会创建一个多路写入器,将日志同时写入文件和回调函数 func (s *ExecuteScriptRequest) getLogWriter() (io.Writer, error) { logPath := s.logFile if !filepath.IsAbs(logPath) { logPath = filepath.Join(s.workDir, logPath) } // 如果文件存在, 清空文件内容 if _, err := os.Stat(logPath); err == nil { if err := os.Truncate(logPath, 0); err != nil { return nil, fmt.Errorf("清空日志文件失败: %v", err) } } var logWriter io.Writer // 如果文件不存在,创建目录和文件, 存在则直接打开 if _, err := os.Stat(logPath); os.IsNotExist(err) { if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { return nil, fmt.Errorf("创建日志目录失败: %v", err) } file, err := os.Create(logPath) if err != nil { return nil, fmt.Errorf("创建日志文件失败: %v", err) } logWriter = file } else { file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return nil, fmt.Errorf("打开日志文件失败: %v", err) } logWriter = file } // 如果有回调函数,创建一个多路写入器 if s.logCallback != nil { return io.MultiWriter(logWriter, &callbackWriter{callback: s.logCallback}), nil } return logWriter, nil } // callbackWriter 实现 io.Writer 接口的回调写入器 type callbackWriter struct { callback func(string) } func (c *callbackWriter) Write(p []byte) (n int, err error) { if c.callback != nil { c.callback(string(p)) } return len(p), nil } // CommandMetadata 命令元数据 type CommandMetadata struct { ID string `json:"id"` // 命令唯一ID Name string `json:"name"` // 命令名称 Description string `json:"description,omitempty"` // 命令描述 Tags []string `json:"tags,omitempty"` // 标签 CreatedBy string `json:"created_by"` // 创建者 CreatedAt time.Time `json:"created_at"` // 创建时间 Timeout time.Duration `json:"timeout"` // 超时时间 EnvVars map[string]string `json:"env_vars"` // 环境变量 WorkDir string `json:"work_dir"` // 工作目录 RefTask string `json:"ref_task,omitempty"` // 关联的任务 } func (s *CommandMetadata) String() string { return fmt.Sprintf("ID=%s, Name=%s, CreatedBy=%s, CreatedAt=%s, Timeout=%s, WorkDir=%s", s.ID, s.Name, s.CreatedBy, s.CreatedAt.Format("2006-01-02 15:04:05"), s.Timeout.String(), s.WorkDir) } // VerifyScriptIntegrity 校验脚本完整性 // ExecutionResult 命令执行结果 type ExecutionResult struct { // 命令 Command string `json:"command"` // 错误原因 Error string `json:"error,omitempty"` // 命令退出码 ExitCode int `json:"exit_code"` // 命令开始执行时间 StartTime time.Time `json:"start_time"` // 命令结束执行时间 EndTime *time.Time `json:"end_time"` // 命令执行时长 Duration time.Duration `json:"duration"` // 命令执行是否成功 Success bool `json:"success"` // 是否跳过执行(跳过视为成功,但标记为 Skip 以便管道状态同步) Skipped bool `json:"skipped,omitempty"` // 非错误的说明信息(比如跳过原因等) Message string `json:"message,omitempty"` // 元数据 Metadata *CommandMetadata `json:"metadata"` // 文件内容集合 FileContents map[string]string `json:"file_contents,omitempty"` // 脚本输出参数(供下一个任务使用) OutputParams map[string]string `json:"output_params,omitempty"` }