This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26435-cli-startup-log4j in repository https://gitbox.apache.org/repos/asf/camel.git
commit 86506a6dc31aef1c2e1a4ebfd8df72a574f93bea Author: Claus Ibsen <[email protected]> AuthorDate: Sat Oct 31 10:48:04 2026 +0100 CAMEL-26435: camel-jbang - The CLI starts log4j for the self-update check, and runs java -version before each command Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/common/InstallDetector.java | 16 +-- .../src/main/resources/bin/camel.bat | 8 ++ .../camel-launcher/src/main/resources/bin/camel.sh | 12 ++- .../dsl/jbang/launcher/CamelBatDiscoveryTest.java | 34 +++++++ .../dsl/jbang/launcher/CamelShDiscoveryTest.java | 51 ++++++++++ .../jbang/launcher/LauncherStartupLoggingTest.java | 111 +++++++++++++++++++++ 6 files changed, 224 insertions(+), 8 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/InstallDetector.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/InstallDetector.java index 2ecaa429672e..1d13aadcb87b 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/InstallDetector.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/InstallDetector.java @@ -24,11 +24,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.function.Predicate; -import org.apache.camel.util.FileUtil; - /** * Detects where the currently running Camel CLI launcher lives, and discovers other Camel CLI installations on the same * machine, across the web installer and the package managers the launcher is also distributed through (Homebrew, @@ -51,6 +50,11 @@ public final class InstallDetector { public record InstallInfo(Path location, InstallMethod method) { } + // not FileUtil.isWindows(): FileUtil has a logger, and the self-update check runs this before each command of the + // launcher, where creating a logger would configure log4j + private static final boolean IS_WINDOWS + = System.getProperty("os.name", "").toLowerCase(Locale.ROOT).startsWith("windows"); + private InstallDetector() { } @@ -102,7 +106,7 @@ public final class InstallDetector { * {@code %LOCALAPPDATA%\Apache Camel\cli\versions} on Windows. */ public static Path webInstallerVersionsRoot() { - if (FileUtil.isWindows()) { + if (IS_WINDOWS) { return Paths.get(System.getenv("LOCALAPPDATA"), "Apache Camel", "cli", "versions"); } String xdgDataHome = System.getenv("XDG_DATA_HOME"); @@ -160,7 +164,7 @@ public final class InstallDetector { public static List<InstallInfo> scanKnownLocations() { List<InstallInfo> found = new ArrayList<>(); scanWebInstaller(found); - if (FileUtil.isWindows()) { + if (IS_WINDOWS) { scanChocolatey(found); scanScoopWindows(found); } else { @@ -173,7 +177,7 @@ public final class InstallDetector { private static void scanWebInstaller(List<InstallInfo> found) { Path root = webInstallerVersionsRoot(); - listVersionDirs(root, dir -> Files.exists(dir.resolve("bin").resolve(FileUtil.isWindows() ? "camel.bat" : "camel.sh"))) + listVersionDirs(root, dir -> Files.exists(dir.resolve("bin").resolve(IS_WINDOWS ? "camel.bat" : "camel.sh"))) .forEach(dir -> found.add(new InstallInfo(dir, InstallMethod.WEB_INSTALLER))); } @@ -240,7 +244,7 @@ public final class InstallDetector { if (path == null || path.isBlank()) { return Optional.empty(); } - String exeName = FileUtil.isWindows() ? "camel.cmd" : "camel"; + String exeName = IS_WINDOWS ? "camel.cmd" : "camel"; for (String dir : path.split(File.pathSeparator)) { if (dir.isBlank()) { continue; diff --git a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat index 2df7eeaa70b5..83d883c93c6e 100644 --- a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat +++ b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.bat @@ -89,6 +89,12 @@ exit /B 1 set "_CAND=%~1" if "%_CAND%"=="" exit /b 1 if not exist "%_CAND%" exit /b 1 +@REM The release file of the JDK (bin\..\release) has the version, which saves starting a JVM for -version. +set "_RAW=" +for %%d in ("%_CAND%") do set "_RELEASE=%%~dpd..\release" +if exist "%_RELEASE%" for /f "tokens=2 delims==" %%v in ('findstr /b /l /c:"JAVA_VERSION=" "%_RELEASE%"') do if not defined _RAW set "_RAW=%%v" +set "_RELEASE=" +if defined _RAW goto tryJavaParseVersion for %%e in ("%_CAND%") do set "_CAND_EXT=%%~xe" set "_PROBE_DIR=" if defined TEMP set "_PROBE_DIR=%TEMP%" @@ -123,6 +129,8 @@ for /f "tokens=3" %%v in ('findstr /b /l /i /c:"java version " /c:"openjdk versi del /q "%_PROBE%" >NUL 2>&1 set "_PROBE=" if not defined _RAW exit /b 1 + +:tryJavaParseVersion set "_RAW=%_RAW:"=%" set "_MAJOR=" for /f "tokens=1,2 delims=." %%a in ("%_RAW%") do ( diff --git a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh index f38fa2edbe37..aac01cf18f02 100644 --- a/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh +++ b/dsl/camel-jbang/camel-launcher/src/main/resources/bin/camel.sh @@ -55,9 +55,17 @@ fi CAMEL_MIN_JAVA=21 # Echo the Java major version of the launcher in $1, or nothing if it cannot be determined. +# The release file of the JDK (bin/../release) has the version, which saves starting a JVM for -version. camel_java_major() { - _cjm_out=`"$1" -version </dev/null 2>&1` || return 1 - _cjm_ver=`echo "$_cjm_out" | sed -n 's/.*version "\([0-9][0-9.]*\).*/\1/p' | head -n 1` + _cjm_ver= + _cjm_release=`dirname "$1"`/../release + if [ -f "$_cjm_release" ]; then + _cjm_ver=`sed -n 's/^JAVA_VERSION="\([0-9][0-9.]*\).*/\1/p' "$_cjm_release" | head -n 1` + fi + if [ -z "$_cjm_ver" ]; then + _cjm_out=`"$1" -version </dev/null 2>&1` || return 1 + _cjm_ver=`echo "$_cjm_out" | sed -n 's/.*version "\([0-9][0-9.]*\).*/\1/p' | head -n 1` + fi [ -n "$_cjm_ver" ] || return 1 case "$_cjm_ver" in 1.*) echo "$_cjm_ver" | cut -d. -f2 ;; diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java index e78134ad0ddc..a3a0a2dda757 100644 --- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelBatDiscoveryTest.java @@ -233,6 +233,40 @@ class CamelBatDiscoveryTest { assertFalse(r.stdout().contains("RAN=OLD"), r.stdout()); } + @Test + void readsTheVersionFromTheReleaseFileOfTheJdk(@TempDir Path base) throws Exception { + // the banner of -version is not a Java version: the version comes from the release file, without -version + Path h = home(base); + Path java = FakeJava.writeFakeJava(base.resolve("jdk").resolve("bin"), "java", GARBAGE, 0, "RELEASE"); + Files.writeString(base.resolve("jdk").resolve("release"), "IMPLEMENTOR=\"Test\"\r\nJAVA_VERSION=\"21.0.3\"\r\n"); + Map<String, String> env = isolatedEnvironment(); + env.put("JAVACMD", java.toString()); + env.put("JAVA_HOME", base.resolve("invalid-java-home").toString()); + + FakeJava.Result r = FakeJava.run(h.resolve("camel.bat"), env, "version"); + + assertEquals(0, r.exitCode(), r.stderr()); + assertTrue(r.stdout().contains("RAN=RELEASE"), r.stdout()); + } + + @Test + void rejectsAnOldJavaFromTheReleaseFileOfTheJdk(@TempDir Path base) throws Exception { + Path h = home(base); + Path old = FakeJava.writeFakeJava(base.resolve("jdk").resolve("bin"), "java", JAVA21, 0, "OLD"); + Files.writeString(base.resolve("jdk").resolve("release"), "JAVA_VERSION=\"17.0.2\"\r\n"); + Path fallback = FakeJava.writeFakeJava(base.resolve("fallback"), "java", JAVA21, 0, "FALLBACK"); + Map<String, String> env = isolatedEnvironment(); + env.put("JAVACMD", old.toString()); + env.put("JAVA_HOME", base.resolve("invalid-java-home").toString()); + env.put("CAMEL_FALLBACK_JAVA", fallback.toString()); + + FakeJava.Result r = FakeJava.run(h.resolve("camel.bat"), env, "version"); + + assertEquals(0, r.exitCode(), r.stderr()); + assertTrue(r.stdout().contains("RAN=FALLBACK"), r.stdout()); + assertFalse(r.stdout().contains("RAN=OLD"), r.stdout()); + } + @Test void rejectsWhenNoCandidateQualifies(@TempDir Path base) throws Exception { Path h = home(base); diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java index 71f15c200677..c3413e75c929 100644 --- a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelShDiscoveryTest.java @@ -312,6 +312,57 @@ class CamelShDiscoveryTest { assertEquals(1, r.exitCode(), "Java 8 must be rejected: " + r.stdout()); } + @Test + void readsTheVersionFromTheReleaseFileOfTheJdk(@TempDir Path base) throws Exception { + // the banner of -version is not a Java version: the version comes from the release file, without -version + Path h = home(base); + Path java = FakeJava.writeFakeJava(base.resolve("jdk/bin"), "java", GARBAGE, 0, "RELEASE"); + Files.writeString(base.resolve("jdk/release"), "IMPLEMENTOR=\"Test\"\nJAVA_VERSION=\"21.0.3\"\n"); + + Map<String, String> env = new HashMap<>(); + env.put("JAVACMD", java.toString()); + env.put("PATH", pathWithNonqualifyingJava(base)); + + FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, "version"); + + assertEquals(0, r.exitCode(), r.stderr()); + assertTrue(r.stdout().contains("RAN=RELEASE"), r.stdout()); + } + + @Test + void rejectsAnOldJavaFromTheReleaseFileOfTheJdk(@TempDir Path base) throws Exception { + Path h = home(base); + Path old = FakeJava.writeFakeJava(base.resolve("jdk/bin"), "java", JAVA21, 0, "OLD"); + Files.writeString(base.resolve("jdk/release"), "JAVA_VERSION=\"17.0.2\"\n"); + Path pathDir = base.resolve("pd"); + FakeJava.writeFakeJava(pathDir, "java", JAVA21, 0, "PATH"); + + Map<String, String> env = new HashMap<>(); + env.put("JAVACMD", old.toString()); + env.put("PATH", pathDir + ":/usr/bin:/bin"); + + FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, "version"); + + assertEquals(0, r.exitCode(), r.stderr()); + assertTrue(r.stdout().contains("RAN=PATH"), r.stdout()); + } + + @Test + void runsJavaVersionWhenTheReleaseFileHasNoVersion(@TempDir Path base) throws Exception { + Path h = home(base); + Path java = FakeJava.writeFakeJava(base.resolve("jdk/bin"), "java", JAVA21, 0, "BANNER"); + Files.writeString(base.resolve("jdk/release"), "IMPLEMENTOR=\"Test\"\n"); + + Map<String, String> env = new HashMap<>(); + env.put("JAVACMD", java.toString()); + env.put("PATH", pathWithNonqualifyingJava(base)); + + FakeJava.Result r = FakeJava.run(h.resolve("camel.sh"), env, "version"); + + assertEquals(0, r.exitCode(), r.stderr()); + assertTrue(r.stdout().contains("RAN=BANNER"), r.stdout()); + } + @Test void handlesSpacesAndUnicodeInJavaPath(@TempDir Path base) throws Exception { Path h = home(base); diff --git a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/LauncherStartupLoggingTest.java b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/LauncherStartupLoggingTest.java new file mode 100644 index 000000000000..c4d837270610 --- /dev/null +++ b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/LauncherStartupLoggingTest.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.launcher; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.dsl.jbang.core.common.InstallDetector; +import org.apache.camel.dsl.jbang.core.common.LauncherHelper; +import org.apache.camel.dsl.jbang.launcher.selfupdate.UpdateChecker; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A command of the launcher that logs nothing does not configure log4j, which takes about 140 ms: it creates no logger + * (creating one configures log4j) and turns the logging off without configuring log4j. Each case runs in a new JVM and + * lists the classes it loads. + */ +class LauncherStartupLoggingTest { + + @TempDir + Path dir; + + @Test + void camelVersionDoesNotConfigureLog4j() throws Exception { + List<String> classes = run(CamelLauncher.class.getName(), "version"); + + assertThat(classes).contains(UpdateChecker.class.getName(), InstallDetector.class.getName()); + assertNoLogging(classes); + } + + @Test + void theUpdateCheckOfTheWebInstallerDoesNotConfigureLog4j() throws Exception { + List<String> classes = run(WebInstallerUpdateCheck.class.getName()); + + // the catalog tells the running version, after the check found the web installer + assertThat(classes).contains(DefaultCamelCatalog.class.getName()); + assertNoLogging(classes); + } + + private static void assertNoLogging(List<String> classes) { + assertThat(classes.stream().filter(c -> c.startsWith("org.slf4j."))).as("no logger").isEmpty(); + assertThat(classes.stream().filter(c -> c.startsWith("org.apache.logging.log4j.core.appender."))) + .as("log4j is not configured").isEmpty(); + } + + // the classes the JVM loads + private List<String> run(String mainClass, String... args) throws Exception { + String java = Path.of(System.getProperty("java.home"), "bin", "java").toString(); + List<String> command = new ArrayList<>( + List.of(java, "-Xlog:class+load=info:file=classes.log:none", "-cp", System.getProperty("java.class.path"), + "-Duser.home=" + dir, mainClass)); + command.addAll(List.of(args)); + ProcessBuilder builder = new ProcessBuilder(command) + .directory(dir.toFile()) + .redirectErrorStream(true) + .redirectOutput(dir.resolve("out.txt").toFile()); + Map<String, String> env = builder.environment(); + env.remove("CAMEL_SELF_UPDATE_CHECK"); + // where the web installer installs (POSIX and Windows) + env.put("XDG_DATA_HOME", dir.toString()); + env.put("LOCALAPPDATA", dir.toString()); + Process process = builder.start(); + assertThat(process.waitFor(60, TimeUnit.SECONDS)).isTrue(); + assertThat(process.exitValue()).as("exit code, output: " + Files.readString(dir.resolve("out.txt"))).isZero(); + List<String> classes = new ArrayList<>(); + for (String line : Files.readAllLines(dir.resolve("classes.log"), StandardCharsets.UTF_8)) { + classes.add(line.split(" ")[0]); + } + return classes; + } + + /** + * The update check of a launcher installed by the web installer. + */ + public static final class WebInstallerUpdateCheck { + + public static void main(String... args) throws Exception { + Path root = InstallDetector.webInstallerVersionsRoot(); + // a cache that is up to date, so that the check does not go to the network + Path cache = root.getParent().resolve("update-check.properties"); + Files.createDirectories(cache.getParent()); + Files.writeString(cache, "last_checked=" + System.currentTimeMillis() + "\nlatest_version=4.0.0\n"); + Path jar = root.resolve("4.0.0").resolve("lib").resolve("camel-launcher.jar"); + System.setProperty(LauncherHelper.CAMEL_LAUNCHER_JAR_PROPERTY, jar.toString()); + UpdateChecker.maybeNotify(new String[] { "version" }); + } + } +}
