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-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"`
|
|
|
|
|
|
}
|