// 脚本运行时探测与本地执行 — 适配 ./aiops 单二进制 / Docker 容器环境 // Logan.Li package scriptrunner import ( "bytes" "context" "fmt" "os" "os/exec" "runtime" "time" ) // RuntimeInfo 当前主机可用的脚本解释器 type RuntimeInfo struct { Shell bool `json:"shell"` ShellPath string `json:"shell_path,omitempty"` Python bool `json:"python"` PythonPath string `json:"python_path,omitempty"` PowerShell bool `json:"powershell"` PowerShellPath string `json:"powershell_path,omitempty"` OS string `json:"os"` Arch string `json:"arch"` EmbedMode bool `json:"embed_mode"` // 单二进制 embed 部署 } // DetectRuntime 探测 bash/sh、python3、powershell 是否可用 // Logan.Li func DetectRuntime() RuntimeInfo { info := RuntimeInfo{ OS: runtime.GOOS, Arch: runtime.GOARCH, EmbedMode: true, } for _, p := range []string{"/bin/bash", "/usr/bin/bash", "bash"} { if path, ok := lookExecutable(p); ok { info.Shell = true info.ShellPath = path break } } if !info.Shell { for _, p := range []string{"/bin/sh", "/usr/bin/sh", "sh"} { if path, ok := lookExecutable(p); ok { info.Shell = true info.ShellPath = path break } } } for _, p := range []string{"/usr/bin/python3", "/usr/local/bin/python3", "python3"} { if path, ok := lookExecutable(p); ok { info.Python = true info.PythonPath = path break } } for _, p := range []string{"/usr/bin/pwsh", "/usr/local/bin/pwsh", "pwsh", "powershell"} { if path, ok := lookExecutable(p); ok { info.PowerShell = true info.PowerShellPath = path break } } return info } func lookExecutable(name string) (string, bool) { path, err := exec.LookPath(name) if err != nil { return "", false } return path, true } // CanRun 判断指定脚本类型是否可在本机执行 // Logan.Li func CanRun(rt RuntimeInfo, scriptType string) (bool, string) { switch scriptType { case "shell", "bash", "command", "": if rt.Shell { return true, "" } return false, "当前环境未找到 bash/sh,无法执行 Shell 脚本(Docker 镜像需安装 bash)" case "python": if rt.Python { return true, "" } return false, "当前环境未找到 python3,无法执行 Python 脚本(请在镜像中安装 python3)" case "powershell": if rt.PowerShell { return true, "" } return false, "当前环境未找到 PowerShell (pwsh),无法执行 PowerShell 脚本" default: return false, fmt.Sprintf("不支持的脚本类型: %s", scriptType) } } // Run 在 AIOps 本机执行脚本,返回合并输出、退出码、耗时(ms) // Logan.Li func Run(scriptType, content string, timeoutSec int) (output string, exitCode int, durationMs int, err error) { rt := DetectRuntime() ok, reason := CanRun(rt, scriptType) if !ok { return reason + "\n", -1, 0, fmt.Errorf("%s", reason) } start := time.Now() timeout := time.Duration(timeoutSec) * time.Second if timeout <= 0 { timeout = 300 * time.Second } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() var cmd *exec.Cmd switch scriptType { case "python": py := rt.PythonPath if py == "" { py = "python3" } cmd = exec.CommandContext(ctx, py, "-c", content) case "powershell": pwsh := rt.PowerShellPath if pwsh == "" { pwsh = "pwsh" } cmd = exec.CommandContext(ctx, pwsh, "-NoProfile", "-NonInteractive", "-Command", content) default: sh := rt.ShellPath if sh == "" { sh = "bash" } cmd = exec.CommandContext(ctx, sh, "-c", content) } var out bytes.Buffer cmd.Stdout = &out cmd.Stderr = &out cmd.Env = os.Environ() runErr := cmd.Run() durationMs = int(time.Since(start).Milliseconds()) output = out.String() if ctx.Err() == context.DeadlineExceeded { output += fmt.Sprintf("\n[超时] 执行超过 %d 秒被终止", timeoutSec) return output, -1, durationMs, ctx.Err() } if runErr != nil { if exitError, ok := runErr.(*exec.ExitError); ok { exitCode = exitError.ExitCode() } else { exitCode = -1 output += "\n" + runErr.Error() } return output, exitCode, durationMs, runErr } return output, 0, durationMs, nil }