- 自动识别 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 补充导航与示例
396 lines
10 KiB
Go
396 lines
10 KiB
Go
package executor
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"gitea.odboy.cn/odboy/kenaito-kopsc/internal/platform"
|
||
)
|
||
|
||
// javaRunOptions are the parsed arguments of `kopsc java run`.
|
||
type javaRunOptions struct {
|
||
jar string // the application jar
|
||
appArgs []string // everything after "--", passed to the application
|
||
jdk string // --jdk: 8 / 11 / 17 / 21
|
||
javaBin string // --java: an explicit java executable
|
||
heap string // --heap / --xmx
|
||
initial string // --xms
|
||
env string // --env
|
||
gcDir string // --gc-log directory
|
||
heapDump bool // --heap-dump-on-oom
|
||
printFlags bool // --print-flags
|
||
quiet bool // -q / --quiet
|
||
}
|
||
|
||
// supportedJDKs are the releases kopsc has a tuning profile for.
|
||
var supportedJDKs = []string{"8", "11", "17", "21"}
|
||
|
||
// runJavaRun launches a Spring-style Java application with kopsc's tuned JVM
|
||
// flags, in the foreground, so its logs and Ctrl+C behave like a plain
|
||
// `java -jar`.
|
||
func runJavaRun(ctx context.Context, req BuiltinRequest) error {
|
||
opts, err := parseJavaRunArgs(req.Args)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
jar, err := resolveJarPath(opts.jar)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
jdk, javaBin, err := resolveJDK(opts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
res := platform.DetectResources()
|
||
tuning := javaTuning{
|
||
JDK: jdk,
|
||
CPUs: res.CPUs,
|
||
Memory: res.Memory,
|
||
Heap: opts.heap,
|
||
Initial: opts.initial,
|
||
Env: opts.env,
|
||
GCDir: opts.gcDir,
|
||
HeapDump: opts.heapDump,
|
||
}
|
||
flags := BuildJavaFlags(tuning)
|
||
argv := append([]string{javaBin}, flags...)
|
||
argv = append(argv, "-jar", jar)
|
||
argv = append(argv, opts.appArgs...)
|
||
|
||
if opts.printFlags {
|
||
fmt.Fprintln(req.Stdout, strings.Join(argv, " "))
|
||
return nil
|
||
}
|
||
|
||
if !opts.quiet {
|
||
fmt.Fprintf(req.Stdout, "JDK: %s(探测到 %s)\n", jdk, describeJava(javaBin))
|
||
fmt.Fprintf(req.Stdout, "资源: %d 核, 内存 %s(%s)\n", res.CPUs, formatBytes(res.Memory), res.Detected)
|
||
fmt.Fprintf(req.Stdout, "堆: %s\n", heapSummary(flags))
|
||
if opts.gcDir != "" {
|
||
fmt.Fprintf(req.Stdout, "GC 日志: %s\n", gcLogPath(opts.gcDir))
|
||
}
|
||
fmt.Fprintf(req.Stdout, "启动: %s\n\n", strings.Join(argv, " "))
|
||
}
|
||
|
||
if opts.gcDir != "" {
|
||
if err := os.MkdirAll(opts.gcDir, 0o755); err != nil {
|
||
return fmt.Errorf("创建 GC 日志目录 %q 失败: %w", opts.gcDir, err)
|
||
}
|
||
}
|
||
|
||
return runJavaForeground(ctx, argv, req)
|
||
}
|
||
|
||
// runJavaForeground runs the JVM attached to the caller's terminal. The exit
|
||
// code is passed through so supervisors see the application's own status.
|
||
//
|
||
// Ctrl+C is forwarded to the JVM instead of killing kopsc outright, so the
|
||
// application's shutdown hooks run and Spring can close its context cleanly.
|
||
func runJavaForeground(ctx context.Context, argv []string, req BuiltinRequest) error {
|
||
cmd := exec.Command(argv[0], argv[1:]...)
|
||
cmd.Stdin = req.Stdin
|
||
cmd.Stdout = req.Stdout
|
||
cmd.Stderr = req.Stderr
|
||
// Relay Ctrl+C/SIGTERM to the JVM so its shutdown hooks run.
|
||
stopSignals := forwardSignals(cmd)
|
||
defer stopSignals()
|
||
|
||
if err := cmd.Start(); err != nil {
|
||
return fmt.Errorf("启动 java 失败: %w", err)
|
||
}
|
||
|
||
// Killing on context cancellation keeps kopsc from outliving its parent
|
||
// while the application itself is still shutting down gracefully.
|
||
done := make(chan struct{})
|
||
go func() {
|
||
select {
|
||
case <-ctx.Done():
|
||
_ = cmd.Process.Kill()
|
||
case <-done:
|
||
}
|
||
}()
|
||
|
||
err := cmd.Wait()
|
||
close(done)
|
||
if err != nil {
|
||
var exitErr *exec.ExitError
|
||
if errors.As(err, &exitErr) {
|
||
return &ExitError{Code: exitErr.ExitCode(), Err: err}
|
||
}
|
||
return fmt.Errorf("等待 java 退出失败: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// resolveJarPath checks that the jar exists and returns an absolute path so the
|
||
// JVM resolves it independently of its working directory.
|
||
func resolveJarPath(jar string) (string, error) {
|
||
info, err := os.Stat(jar)
|
||
if err != nil {
|
||
return "", fmt.Errorf("找不到应用 jar %q: %w;用法: kopsc java run <app.jar> [-- 应用参数...]", jar, err)
|
||
}
|
||
if info.IsDir() {
|
||
return "", fmt.Errorf("%q 是目录,不是 jar 文件", jar)
|
||
}
|
||
abs, err := filepath.Abs(jar)
|
||
if err != nil {
|
||
return jar, nil
|
||
}
|
||
return abs, nil
|
||
}
|
||
|
||
// resolveJDK decides which JDK to tune for and which java executable to run.
|
||
func resolveJDK(opts javaRunOptions) (version, javaBin string, err error) {
|
||
if opts.jdk != "" {
|
||
version = normalizeJDKFlag(opts.jdk)
|
||
if !containsString(supportedJDKs, version) {
|
||
return "", "", fmt.Errorf("--jdk 只支持 %s,收到 %q",
|
||
strings.Join(supportedJDKs, "/"), opts.jdk)
|
||
}
|
||
return version, javaExecutable(opts.javaBin), nil
|
||
}
|
||
|
||
var info platform.Java
|
||
if opts.javaBin != "" {
|
||
info = platform.DetectJavaAt(opts.javaBin)
|
||
} else {
|
||
info = platform.DetectJava()
|
||
}
|
||
if info.Version == "" {
|
||
return "", "", fmt.Errorf("无法自动识别 JDK 版本(%s);请用 --jdk 指定:%s",
|
||
describeJavaDetection(info), strings.Join(supportedJDKs, " / "))
|
||
}
|
||
if !containsString(supportedJDKs, info.Version) {
|
||
return "", "", fmt.Errorf("检测到 JDK %s,kopsc 暂无该版本的调优方案;"+
|
||
"可用 --jdk 指定按 %s 的参数运行", info.Raw, strings.Join(supportedJDKs, " / "))
|
||
}
|
||
return info.Version, firstNonEmptyString(info.Path, javaExecutable(opts.javaBin)), nil
|
||
}
|
||
|
||
// javaExecutable returns the java binary to run: --java, or "java" on PATH.
|
||
func javaExecutable(explicit string) string {
|
||
if explicit != "" {
|
||
return explicit
|
||
}
|
||
return "java"
|
||
}
|
||
|
||
// normalizeJDKFlag accepts "8", "1.8", "jdk8", "JDK 17" and similar spellings.
|
||
func normalizeJDKFlag(value string) string {
|
||
v := strings.ToLower(strings.TrimSpace(value))
|
||
v = strings.TrimPrefix(v, "jdk")
|
||
v = strings.TrimSpace(v)
|
||
if strings.HasPrefix(v, "1.") {
|
||
v = strings.TrimPrefix(v, "1.")
|
||
}
|
||
if i := strings.IndexAny(v, "._"); i > 0 {
|
||
v = v[:i]
|
||
}
|
||
if _, err := strconv.Atoi(v); err != nil {
|
||
return value
|
||
}
|
||
return v
|
||
}
|
||
|
||
// describeJava renders the java executable for the startup summary.
|
||
func describeJava(path string) string {
|
||
if path == "" {
|
||
return "java(PATH 中)"
|
||
}
|
||
return path
|
||
}
|
||
|
||
func describeJavaDetection(info platform.Java) string {
|
||
if info.Path == "" {
|
||
return "PATH 中没有 java"
|
||
}
|
||
return "java 存在但版本无法解析"
|
||
}
|
||
|
||
// heapSummary extracts the -Xmx value for the startup summary.
|
||
func heapSummary(flags []string) string {
|
||
for _, f := range flags {
|
||
if strings.HasPrefix(f, "-Xmx") {
|
||
return strings.TrimPrefix(f, "-Xmx") + "(-Xms 相同或由 --xms 指定)"
|
||
}
|
||
}
|
||
return "交由 JVM 自行决定(未识别到内存)"
|
||
}
|
||
|
||
// formatBytes renders a byte count for humans; "未知" when it is 0.
|
||
func formatBytes(n int64) string {
|
||
if n <= 0 {
|
||
return "未知"
|
||
}
|
||
return humanSize(n)
|
||
}
|
||
|
||
func containsString(values []string, want string) bool {
|
||
for _, v := range values {
|
||
if v == want {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func firstNonEmptyString(values ...string) string {
|
||
for _, v := range values {
|
||
if v != "" {
|
||
return v
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// parseJavaRunArgs parses kopsc's options, which must come before the first
|
||
// positional argument (the jar). Everything after "--" is handed to the
|
||
// application untouched.
|
||
func parseJavaRunArgs(args []string) (javaRunOptions, error) {
|
||
var opts javaRunOptions
|
||
i := 0
|
||
for ; i < len(args); i++ {
|
||
arg := args[i]
|
||
|
||
if arg == "--" {
|
||
// `-- jar args` : the jar follows the separator.
|
||
i++
|
||
break
|
||
}
|
||
if !strings.HasPrefix(arg, "-") || arg == "-" {
|
||
break
|
||
}
|
||
|
||
name, value := arg, ""
|
||
if eq := strings.IndexByte(arg, '='); eq >= 0 && strings.HasPrefix(arg, "--") {
|
||
name, value = arg[:eq], arg[eq+1:]
|
||
}
|
||
takeValue := func() (string, error) {
|
||
if value != "" {
|
||
return value, nil
|
||
}
|
||
if i+1 >= len(args) {
|
||
return "", fmt.Errorf("%s 缺少参数", name)
|
||
}
|
||
i++
|
||
return args[i], nil
|
||
}
|
||
|
||
switch name {
|
||
case "--jdk":
|
||
v, err := takeValue()
|
||
if err != nil {
|
||
return opts, err
|
||
}
|
||
opts.jdk = v
|
||
case "--java":
|
||
v, err := takeValue()
|
||
if err != nil {
|
||
return opts, err
|
||
}
|
||
opts.javaBin = v
|
||
case "--heap", "--xmx":
|
||
v, err := takeValue()
|
||
if err != nil {
|
||
return opts, err
|
||
}
|
||
if parseJVMSize(v) <= 0 {
|
||
return opts, fmt.Errorf("--heap 无法识别大小 %q,可用 2g、512m 或字节数", v)
|
||
}
|
||
opts.heap = v
|
||
case "--xms":
|
||
v, err := takeValue()
|
||
if err != nil {
|
||
return opts, err
|
||
}
|
||
if parseJVMSize(v) <= 0 {
|
||
return opts, fmt.Errorf("--xms 无法识别大小 %q,可用 2g、512m 或字节数", v)
|
||
}
|
||
opts.initial = v
|
||
case "--env":
|
||
v, err := takeValue()
|
||
if err != nil {
|
||
return opts, err
|
||
}
|
||
opts.env = v
|
||
case "--gc-log":
|
||
v, err := takeValue()
|
||
if err != nil {
|
||
return opts, err
|
||
}
|
||
opts.gcDir = v
|
||
case "--no-gc-log":
|
||
opts.gcDir = "" // explicit, and the default is off
|
||
case "--heap-dump-on-oom":
|
||
opts.heapDump = true
|
||
case "--print-flags":
|
||
opts.printFlags = true
|
||
case "-q", "--quiet":
|
||
opts.quiet = true
|
||
default:
|
||
return opts, fmt.Errorf("未知选项 %q;应用自己的参数请写在 jar 之后,或用 -- 分隔", name)
|
||
}
|
||
}
|
||
|
||
if i >= len(args) {
|
||
return opts, errors.New("缺少应用 jar;用法: kopsc java run <app.jar> [-- 应用参数...]")
|
||
}
|
||
opts.jar = args[i]
|
||
rest := args[i+1:]
|
||
// A separator after the jar is not an application argument.
|
||
if len(rest) > 0 && rest[0] == "--" {
|
||
rest = rest[1:]
|
||
}
|
||
opts.appArgs = append(opts.appArgs, rest...)
|
||
return opts, nil
|
||
}
|
||
|
||
// runJavaPreview renders the command `kopsc java run` would execute without
|
||
// starting anything. It backs both --dry-run and --print-flags, so the same
|
||
// resolution code path is used; only the output differs.
|
||
func runJavaPreview(ctx context.Context, req BuiltinRequest) error {
|
||
opts, err := parseJavaRunArgs(req.Args)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// The jar may not exist yet in a dry run; report it rather than failing so
|
||
// the flags can still be inspected.
|
||
if _, statErr := os.Stat(opts.jar); statErr != nil {
|
||
fmt.Fprintf(req.Stderr, "注意: %v\n", statErr)
|
||
}
|
||
|
||
jdk, javaBin, err := resolveJDK(opts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
res := platform.DetectResources()
|
||
flags := BuildJavaFlags(javaTuning{
|
||
JDK: jdk,
|
||
CPUs: res.CPUs,
|
||
Memory: res.Memory,
|
||
Heap: opts.heap,
|
||
Initial: opts.initial,
|
||
Env: opts.env,
|
||
GCDir: opts.gcDir,
|
||
HeapDump: opts.heapDump,
|
||
})
|
||
argv := append([]string{javaBin}, flags...)
|
||
argv = append(argv, "-jar", opts.jar)
|
||
argv = append(argv, opts.appArgs...)
|
||
|
||
fmt.Fprintln(req.Stdout, "将要启动的 java 命令:")
|
||
fmt.Fprintln(req.Stdout, strings.Join(argv, " "))
|
||
fmt.Fprintf(req.Stdout, "\n资源: %d 核, 内存 %s(%s)\n", res.CPUs, formatBytes(res.Memory), res.Detected)
|
||
return nil
|
||
}
|