Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions configs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ autodetect:
- /usr/lib/jvm/java-1.8.0
- /usr/lib/jvm/java-1.8.0-oracle
- /usr/lib/jvm/bellsoft-java8-amd64
- /usr/lib/jvm/jre
- /usr/lib/jvm/java-arenadata-openjdk-8

"17":
Expand Down
43 changes: 39 additions & 4 deletions internal/detect/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,24 @@ import (
"strings"

"github.com/arenadata/ad-runtime-utils/internal/config"
"github.com/arenadata/ad-runtime-utils/internal/fs"
)

func hasGlobMeta(s string) bool {
return strings.ContainsAny(s, "*?[")
}

func checkCandidate(cand, exe string) (string, bool) {
resolved, evalErr := filepath.EvalSymlinks(cand)
if evalErr != nil || resolved == "" {
resolved = cand
}
candidateExe := filepath.Join(resolved, "bin", exe)
if _, statErr := os.Stat(candidateExe); statErr == nil {
return resolved, true
}
return "", false
}

// expandPath expands a leading '~' to the user home directory and
// replaces any environment variables in the path.
func expandPath(p string) string {
Expand Down Expand Up @@ -57,11 +72,31 @@ func tryEnvVar(cfg config.RuntimeSetting, exe string) (string, bool) {
func tryPaths(cfg config.RuntimeSetting, exe string) (string, bool) {
for _, pat := range cfg.Paths {
base := expandPath(pat)
cands, _ := filepath.Glob(base)

if hasGlobMeta(base) {
cands, _ := filepath.Glob(base)
sort.Sort(sort.Reverse(sort.StringSlice(cands)))
for _, cand := range cands {
if p, ok := checkCandidate(cand, exe); ok {
return p, true
}
}
continue
}

if _, statErr := os.Stat(base); statErr == nil {
if p, ok := checkCandidate(base, exe); ok {
return p, true
}
continue
}

globPat := base + "*"
cands, _ := filepath.Glob(globPat)
sort.Sort(sort.Reverse(sort.StringSlice(cands)))
for _, cand := range cands {
if fs.ExistsInBin(cand, exe) {
return cand, true
if p, ok := checkCandidate(cand, exe); ok {
return p, true
}
}
}
Expand Down
115 changes: 115 additions & 0 deletions internal/detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,118 @@ func TestDetectPath_Order(t *testing.T) {
t.Errorf("detectPath empty = (%q, %v), want ('', false)", got, ok)
}
}
func TestTryPaths_ExactSymlinkResolves(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
tmp := t.TempDir()
target := createRuntimeDir(t, tmp, "jdk-real", "java")

altsDir := filepath.Join(tmp, "alternatives")
if err := os.MkdirAll(altsDir, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
link := filepath.Join(altsDir, "jre")
if err := os.Symlink(target, link); err != nil {
t.Fatalf("symlink: %v", err)
}

rt := config.RuntimeSetting{Paths: []string{link}}
got, ok := tryPaths(rt, "java")
if !ok {
t.Fatalf("tryPaths symlink failed")
}
if got != target {
t.Errorf("got %q, want symlink target %q", got, target)
}
}

func TestTryPaths_ExactExistsButInvalid_NoGlobFallback(t *testing.T) {
tmp := t.TempDir()
exact := filepath.Join(tmp, "java-1.8.0")
if err := os.MkdirAll(exact, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
valid := createRuntimeDir(t, tmp, "java-1.8.0-openjdk-1.8.0.412", "java")

rt := config.RuntimeSetting{Paths: []string{exact}}
if p, ok := tryPaths(rt, "java"); ok || p != "" {
t.Errorf("expected no detection, got (%q, %v) with valid sibling %q", p, ok, valid)
}
}

func TestTryPaths_PrefixWhenBaseNotExists_GlobsAndFinds(t *testing.T) {
tmp := t.TempDir()
prefix := filepath.Join(tmp, "jdk-17")
_ = os.RemoveAll(prefix)

_ = createRuntimeDir(t, tmp, "jdk-17.0.8", "java")
_ = createRuntimeDir(t, tmp, "jdk-17.0.7", "java")

rt := config.RuntimeSetting{Paths: []string{prefix}}
got, ok := tryPaths(rt, "java")
if !ok {
t.Fatalf("expected detection via prefix glob")
}

cands, _ := filepath.Glob(prefix + "*")
sort.Sort(sort.Reverse(sort.StringSlice(cands)))
want := cands[0]
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}

func TestTryPaths_GlobPatternKeptVerbatim(t *testing.T) {
tmp := t.TempDir()
_ = createRuntimeDir(t, tmp, "liberica-jre-17.0.9+9", "java")
_ = createRuntimeDir(t, tmp, "liberica-jre-17.0.8+8", "java")

pattern := filepath.Join(tmp, "liberica-jre-17*")
rt := config.RuntimeSetting{Paths: []string{pattern}}

got, ok := tryPaths(rt, "java")
if !ok {
t.Fatalf("glob pattern failed")
}
cands, _ := filepath.Glob(pattern)
sort.Sort(sort.Reverse(sort.StringSlice(cands)))
if got != cands[0] {
t.Errorf("got %q, want top %q", got, cands[0])
}
}

func TestTryPaths_SymlinkGlob(t *testing.T) {
tmp := t.TempDir()
target := createRuntimeDir(t, tmp, "jdk-real-21", "java")
link := filepath.Join(tmp, "jre-21") // имя под паттерн
if err := os.Symlink(target, link); err != nil {
t.Fatalf("symlink: %v", err)
}

pattern := filepath.Join(tmp, "jre-21*")
rt := config.RuntimeSetting{Paths: []string{pattern}}

got, ok := tryPaths(rt, "java")
if !ok {
t.Fatalf("symlink via glob failed")
}
if got != target {
t.Errorf("got %q, want resolved %q", got, target)
}
}

func TestTryPaths_NoBinSkip(t *testing.T) {
tmp := t.TempDir()
bad := filepath.Join(tmp, "jdk-23.0.0")
if err := os.MkdirAll(bad, 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
good := createRuntimeDir(t, tmp, "jdk-23.0.1", "java")

rt := config.RuntimeSetting{Paths: []string{filepath.Join(tmp, "jdk-23*")}}
got, ok := tryPaths(rt, "java")
if !ok || got != good {
t.Errorf("got (%q,%v), want (%q,true)", got, ok, good)
}
}
45 changes: 39 additions & 6 deletions scripts/bigtop-detect-javahome
Original file line number Diff line number Diff line change
@@ -1,24 +1,57 @@
#!/usr/bin/env bash
#
# Wrapper for ad-runtime-utils that detects JAVA_HOME.
# Safe to be sourced (will return on error instead of exiting the shell).

# only run if JAVA_HOME is unset
# Detect if this script is being sourced instead of executed.
_is_sourced() {
# In bash, sourced if BASH_SOURCE[0] != $0
[[ "${BASH_SOURCE[0]}" != "$0" ]]
}

# Unified error handler: print an error message, then either return (if sourced)
# or exit (if executed as a normal script).
_die() {
echo "Error running ad-runtime-utils: $*" >&2
if _is_sourced; then
return 1
else
exit 1
fi
}

# Only run detection if JAVA_HOME is not already set
if [ -z "$JAVA_HOME" ]; then
# run the detector, capture both stdout and stderr
# Run the detector and capture both stdout and stderr
EXPORT_CMD=$(
/usr/lib/ad-runtime-utils/bin/ad-runtime-utils \
--config "/etc/ad-runtime-utils/config.yaml" \
--service "${ADH_SERVICE_NAME:-}" \
--runtime java 2>&1
)
RET=$?

# If the binary failed, print the message and stop (return or exit)
if [ $RET -ne 0 ]; then
# on error, print the message and exit with the same code
echo "Error running ad-runtime-utils: $EXPORT_CMD" >&2
exit $RET
_die "$EXPORT_CMD"
return 1 2>/dev/null || exit 1
fi

# if we got back an export line, eval it in this shell
# If we got something back, validate and evaluate it
if [ -n "$EXPORT_CMD" ]; then
# Safety check: only accept an "export JAVA_HOME=..." line
case "$EXPORT_CMD" in
export\ JAVA_HOME=*) : ;; # OK
*)
_die "unexpected output: $EXPORT_CMD"
return 1 2>/dev/null || exit 1
;;
esac

# Evaluate the export in the current shell
eval "$EXPORT_CMD"

# Optional debug output
if [ -n "$AD_UTILS_DEBUG" ]; then
echo "Using detected JAVA_HOME: $JAVA_HOME"
fi
Expand Down