- 自动识别 JDK 8/11/17/21 与宿主机 CPU、可用内存;容器内以 cgroup 限额为准;堆默认取可用内存 50%,-Xms 与 -Xmx 同值 - 按版本分支生成参数:JDK 8 用 -Xloggc 一组,JDK 9+ 用 -Xlog:; 堆超过 30g 时自动省略压缩指针参数 - 支持 --jdk/--java/--heap/--xms/--env/--gc-log/--heap-dump-on-oom/ --print-flags/--dry-run;jar 之后的参数原样交给应用 - 前台运行:stdio 直连、退出码透传、SIGINT/SIGTERM 转发给 JVM - BuiltinRequest 增加 Stdin,使内置命令可以读取调用方输入 - 修正帮助中的用法行重复(自定义 Usage 不再追加通用透传尾巴) - 文档:新增 README.java.md,主 README 补充导航与示例
229 lines
6.0 KiB
Go
229 lines
6.0 KiB
Go
// Package executor turns a resolved variant into a process, with dry-run
|
||
// support and faithful stdio/exit-code passthrough.
|
||
package executor
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"os/exec"
|
||
"runtime"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"gitea.odboy.cn/odboy/kenaito-kopsc/internal/resolve"
|
||
)
|
||
|
||
// Options controls how a Plan is executed.
|
||
type Options struct {
|
||
DryRun bool
|
||
Stdin io.Reader
|
||
Stdout io.Writer
|
||
Stderr io.Writer
|
||
Env []string // optional override of the child environment
|
||
Dir string // optional working directory
|
||
}
|
||
|
||
// Plan is a resolved variant plus arguments forwarded from the caller.
|
||
type Plan struct {
|
||
Result resolve.Result
|
||
ExtraArgs []string
|
||
}
|
||
|
||
// Argv returns the final argument vector: the variant's argv followed by the
|
||
// forwarded arguments.
|
||
func (p Plan) Argv() []string {
|
||
argv := make([]string, 0, len(p.Result.Variant.Argv)+len(p.ExtraArgs))
|
||
argv = append(argv, p.Result.Variant.Argv...)
|
||
argv = append(argv, p.ExtraArgs...)
|
||
return argv
|
||
}
|
||
|
||
// Display renders the command line the plan would run.
|
||
func (p Plan) Display() string {
|
||
if p.Result.Variant.Builtin != "" {
|
||
parts := append([]string{"(内置)", p.Result.Variant.Builtin}, p.ExtraArgs...)
|
||
return strings.Join(parts, " ")
|
||
}
|
||
argv := p.Argv()
|
||
if p.Result.Variant.Shell {
|
||
flag := "-c"
|
||
if runtime.GOOS == "windows" {
|
||
flag = "/c"
|
||
}
|
||
return shellName() + " " + flag + " " + quoteForShell(strings.Join(argv, " "), runtime.GOOS)
|
||
}
|
||
return strings.Join(argv, " ")
|
||
}
|
||
|
||
// PreviewBuiltin returns the builtin advertised by the resolved command for
|
||
// listing what it would touch, or "" when the command has none.
|
||
func (p Plan) PreviewBuiltin() string {
|
||
return p.Result.Command.ConfirmBuiltin
|
||
}
|
||
|
||
// Explain writes why this variant was chosen and what will run.
|
||
func (p Plan) Explain(w io.Writer) {
|
||
if w == nil {
|
||
return
|
||
}
|
||
variant := p.Result.Variant
|
||
fmt.Fprintf(w, "将执行: %s\n", p.Display())
|
||
fmt.Fprintf(w, "平台: %s\n", resolve.DescribeInfo(p.Result.Info))
|
||
switch {
|
||
case variant.Builtin != "":
|
||
fmt.Fprintf(w, "方式: 内置实现(%s,不启动外部进程)\n", variant.Builtin)
|
||
case variant.Shell:
|
||
fmt.Fprintf(w, "方式: shell (%s)\n", shellName())
|
||
default:
|
||
fmt.Fprintf(w, "方式: 直接执行\n")
|
||
}
|
||
note := variant.Note
|
||
if note == "" {
|
||
note = "无备注"
|
||
}
|
||
fmt.Fprintf(w, "命中: 变体 #%d(%d 分)%s\n", p.Result.VariantIdx, p.Result.Score, note)
|
||
if p.Result.VariantIdx >= 0 && p.Result.VariantIdx < len(p.Result.Candidates) {
|
||
fmt.Fprintf(w, "依据: %s\n", p.Result.Candidates[p.Result.VariantIdx].Reason)
|
||
}
|
||
if len(p.ExtraArgs) > 0 {
|
||
fmt.Fprintf(w, "透传: %s\n", strings.Join(p.ExtraArgs, " "))
|
||
}
|
||
}
|
||
|
||
// Run executes the plan. With DryRun it only prints the explanation. A child
|
||
// exiting non-zero is reported as *ExitError so callers can preserve the code.
|
||
func (p Plan) Run(ctx context.Context, opts Options) error {
|
||
if opts.Stdout == nil {
|
||
opts.Stdout = io.Discard
|
||
}
|
||
if opts.Stderr == nil {
|
||
opts.Stderr = io.Discard
|
||
}
|
||
if opts.DryRun {
|
||
fmt.Fprintln(opts.Stdout, "[dry-run] 未执行任何命令")
|
||
p.Explain(opts.Stdout)
|
||
return nil
|
||
}
|
||
|
||
if variant := p.Result.Variant; variant.Builtin != "" {
|
||
fn, ok := LookupBuiltin(variant.Builtin)
|
||
if !ok {
|
||
return fmt.Errorf("未注册的内置动作 %q", variant.Builtin)
|
||
}
|
||
return fn(ctx, BuiltinRequest{
|
||
Args: p.ExtraArgs,
|
||
Stdin: opts.Stdin,
|
||
Stdout: opts.Stdout,
|
||
Stderr: opts.Stderr,
|
||
Info: p.Result.Info,
|
||
Plan: p,
|
||
})
|
||
}
|
||
|
||
name, args := p.command()
|
||
cmd := exec.CommandContext(ctx, name, args...)
|
||
cmd.Stdin = opts.Stdin
|
||
cmd.Stdout = opts.Stdout
|
||
cmd.Stderr = opts.Stderr
|
||
cmd.Env = opts.Env
|
||
cmd.Dir = opts.Dir
|
||
|
||
if err := cmd.Run(); err != nil {
|
||
var exitErr *exec.ExitError
|
||
if errors.As(err, &exitErr) {
|
||
return &ExitError{Code: exitErr.ExitCode(), Err: err}
|
||
}
|
||
return fmt.Errorf("执行 %q 失败: %w", name, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (p Plan) command() (string, []string) {
|
||
argv := p.Argv()
|
||
if !p.Result.Variant.Shell {
|
||
return argv[0], argv[1:]
|
||
}
|
||
shell := shellName()
|
||
if runtime.GOOS == "windows" {
|
||
return shell, []string{"/c", joinForShell(argv, runtime.GOOS)}
|
||
}
|
||
return shell, []string{"-c", joinForShell(argv, runtime.GOOS)}
|
||
}
|
||
|
||
func shellName() string {
|
||
if runtime.GOOS == "windows" {
|
||
return "cmd"
|
||
}
|
||
return "sh"
|
||
}
|
||
|
||
// joinForShell joins argv with platform-appropriate quoting.
|
||
func joinForShell(argv []string, osName string) string {
|
||
parts := make([]string, len(argv))
|
||
for i, arg := range argv {
|
||
parts[i] = quoteForShell(arg, osName)
|
||
}
|
||
return strings.Join(parts, " ")
|
||
}
|
||
|
||
// quoteForShell leaves safe arguments untouched and quotes the rest.
|
||
func quoteForShell(arg, osName string) string {
|
||
if osName == "windows" {
|
||
if arg != "" && !strings.ContainsAny(arg, " \t\"&|<>^%") {
|
||
return arg
|
||
}
|
||
return `"` + strings.ReplaceAll(arg, `"`, `\"`) + `"`
|
||
}
|
||
|
||
if arg != "" && strings.IndexFunc(arg, unsafeShellRune) < 0 {
|
||
return arg
|
||
}
|
||
return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'"
|
||
}
|
||
|
||
func unsafeShellRune(r rune) bool {
|
||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||
return false
|
||
}
|
||
return !strings.ContainsRune("-_.:/=@+,", r)
|
||
}
|
||
|
||
// ExitError carries the child process exit code.
|
||
type ExitError struct {
|
||
Code int
|
||
Err error
|
||
}
|
||
|
||
func (e *ExitError) Error() string {
|
||
// When the error carries its own explanation (for example a timeout that
|
||
// lists what is still running), show it instead of the bare status code.
|
||
if e.Err != nil {
|
||
if msg := e.Err.Error(); msg != "" && !isExitStatus(msg) {
|
||
return msg
|
||
}
|
||
}
|
||
return fmt.Sprintf("命令以状态码 %d 退出", e.Code)
|
||
}
|
||
|
||
// isExitStatus reports whether msg is just an os/exec exit status, which adds
|
||
// nothing to kopsc's own wording.
|
||
func isExitStatus(msg string) bool {
|
||
return strings.HasPrefix(msg, "exit status ") || strings.HasPrefix(msg, "signal: ")
|
||
}
|
||
|
||
func (e *ExitError) Unwrap() error { return e.Err }
|
||
|
||
// ExitCode maps an error to the process exit code kopsc should return.
|
||
func ExitCode(err error) int {
|
||
if err == nil {
|
||
return 0
|
||
}
|
||
var exitErr *ExitError
|
||
if errors.As(err, &exitErr) {
|
||
return exitErr.Code
|
||
}
|
||
return 1
|
||
}
|