go20/devops/agent/script/interface.go
2026-03-29 11:41:32 +08:00

265 lines
8.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package script
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/infraboard/mcube/v2/ioc"
)
const (
APP_NAME = "script_excutor"
)
func GetWorkDirAbsPath(workDir string) (string, error) {
executor := ioc.Controller().Get(APP_NAME).(*ScriptExcutor)
return executor.GetWorkDirAbsPath(workDir)
}
// 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, // 默认使用进程组杀死方式
timeout: 60 * time.Minute,
logFile: "stdout.log",
resultFile: "result.json",
}
}
// 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"`
}