go20/devops/agent/script/interface.go

265 lines
8.4 KiB
Go
Raw Normal View History

2026-03-08 18:05:17 +08:00
package script
import (
2026-03-15 16:24:01 +08:00
"context"
2026-03-08 18:05:17 +08:00
"fmt"
2026-03-15 16:24:01 +08:00
"io"
2026-03-08 18:05:17 +08:00
"os"
"os/exec"
2026-03-15 16:24:01 +08:00
"path/filepath"
2026-03-08 18:05:17 +08:00
"strings"
"time"
2026-03-15 16:24:01 +08:00
"github.com/infraboard/mcube/v2/ioc"
)
const (
APP_NAME = "script_excutor"
2026-03-08 18:05:17 +08:00
)
2026-03-29 11:41:32 +08:00
func GetWorkDirAbsPath(workDir string) (string, error) {
executor := ioc.Controller().Get(APP_NAME).(*ScriptExcutor)
return executor.GetWorkDirAbsPath(workDir)
}
2026-03-15 16:24:01 +08:00
// 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, // 默认使用进程组杀死方式
2026-03-15 17:03:15 +08:00
timeout: 60 * time.Minute,
logFile: "stdout.log",
resultFile: "result.json",
2026-03-15 16:24:01 +08:00
}
}
2026-03-08 18:05:17 +08:00
// 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
}
2026-03-15 16:24:01 +08:00
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
}
2026-03-08 18:05:17 +08:00
// 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
}
}
2026-03-15 16:24:01 +08:00
// SetDebugScript 设置是否启用调试脚本
// 通过环境变量 DEBUG_SCRIPT 控制 (值为 true 或 1 时启用)
func (s *ExecuteScriptRequest) SetDebugScript(v bool) {
if v {
s.envVars["DEBUG_SCRIPT"] = "true"
} else {
s.envVars["DEBUG_SCRIPT"] = "false"
}
}
2026-03-08 18:05:17 +08:00
// 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
}
2026-03-15 16:24:01 +08:00
// 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
}
2026-03-08 18:05:17 +08:00
// 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"` // 关联的任务
}
2026-03-15 16:24:01 +08:00
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 校验脚本完整性
2026-03-08 18:05:17 +08:00
// 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"`
}