Skip to content

Commit 09cb310

Browse files
committed
Enhance version.sh and version.bat output to display APR, OpenSSL, and third-party library versions
1 parent 29eb107 commit 09cb310

8 files changed

Lines changed: 804 additions & 2 deletions

File tree

bin/catalina.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ set CATALINA_OPTS=
305305
goto execCmd
306306

307307
:doVersion
308-
%_EXECJAVA% %JAVA_OPTS% -classpath "%CATALINA_HOME%\lib\catalina.jar" org.apache.catalina.util.ServerInfo
308+
%_EXECJAVA% %JAVA_OPTS% -classpath "%CATALINA_HOME%\bin\tomcat-juli.jar;%CATALINA_HOME%\lib\*" -Dcatalina.home="%CATALINA_HOME%" -Dcatalina.base="%CATALINA_BASE%" org.apache.catalina.util.ServerInfo
309309
goto end
310310

311311

bin/catalina.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,9 @@ elif [ "$1" = "configtest" ] ; then
564564
elif [ "$1" = "version" ] ; then
565565

566566
eval "\"$_RUNJAVA\"" "$JAVA_OPTS" \
567-
-classpath "\"$CATALINA_HOME/lib/catalina.jar\"" \
567+
-classpath "\"$CATALINA_HOME/bin/tomcat-juli.jar:$CATALINA_HOME/lib/*\"" \
568+
-Dcatalina.home="\"$CATALINA_HOME\"" \
569+
-Dcatalina.base="\"$CATALINA_BASE\"" \
568570
org.apache.catalina.util.ServerInfo
569571

570572
else

java/org/apache/catalina/core/AprLifecycleListener.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,96 @@ public static boolean isAprAvailable() {
122122
return org.apache.tomcat.jni.AprStatus.isAprAvailable();
123123
}
124124

125+
/**
126+
* Helper method to safely get a version string from APR/TCN.
127+
* Checks APR availability and handles exceptions.
128+
*
129+
* @param versionSupplier supplier that returns the version string
130+
* @return the version string, or null if APR is not available or an error occurs
131+
*/
132+
private static String getVersionString(java.util.function.Supplier<String> versionSupplier) {
133+
if (!isAprAvailable()) {
134+
return null;
135+
}
136+
137+
try {
138+
return versionSupplier.get();
139+
} catch (Exception e) {
140+
return null;
141+
}
142+
}
143+
144+
/**
145+
* Get the installed Tomcat Native version string, if available.
146+
*
147+
* @return the version string, or null if APR is not available
148+
*/
149+
public static String getInstalledTcnVersion() {
150+
return getVersionString(org.apache.tomcat.jni.Library::versionString);
151+
}
152+
153+
/**
154+
* Get the installed APR version string, if available.
155+
*
156+
* @return the APR version string, or null if APR is not available
157+
*/
158+
public static String getInstalledAprVersion() {
159+
return getVersionString(org.apache.tomcat.jni.Library::aprVersionString);
160+
}
161+
162+
/**
163+
* Get the installed OpenSSL version string (via APR), if available.
164+
*
165+
* @return the OpenSSL version string, or null if not available
166+
*/
167+
public static String getInstalledOpenSslVersion() {
168+
return getVersionString(org.apache.tomcat.jni.SSL::versionString);
169+
}
170+
171+
/**
172+
* Helper method to convert version components to a comparable integer.
173+
*
174+
* @param major major version number
175+
* @param minor minor version number
176+
* @param patch patch version number
177+
*
178+
* @return comparable version integer
179+
*/
180+
private static int versionToInt(int major, int minor, int patch) {
181+
return major * 1000 + minor * 100 + patch;
182+
}
183+
184+
/**
185+
* Get a warning message if the installed Tomcat Native version is older than recommended.
186+
* This performs the same version check used during Tomcat startup.
187+
*
188+
* @return a warning message if the installed version is outdated, or null if the version
189+
* is acceptable or APR is not available
190+
*/
191+
public static String getTcnVersionWarning() {
192+
if (!isAprAvailable()) {
193+
return null;
194+
}
195+
196+
try {
197+
int installedVersion = versionToInt(
198+
org.apache.tomcat.jni.Library.TCN_MAJOR_VERSION,
199+
org.apache.tomcat.jni.Library.TCN_MINOR_VERSION,
200+
org.apache.tomcat.jni.Library.TCN_PATCH_VERSION);
201+
int recommendedVersion = versionToInt(
202+
TCN_RECOMMENDED_MAJOR,
203+
TCN_RECOMMENDED_MINOR,
204+
TCN_RECOMMENDED_PV);
205+
if (installedVersion < recommendedVersion) {
206+
return "WARNING: Tomcat recommends a minimum version of " +
207+
TCN_RECOMMENDED_MAJOR + "." + TCN_RECOMMENDED_MINOR + "." + TCN_RECOMMENDED_PV;
208+
}
209+
return null;
210+
} catch (Exception e) {
211+
return null;
212+
}
213+
}
214+
125215
public AprLifecycleListener() {
126216
org.apache.tomcat.jni.AprStatus.setInstanceCreated(true);
127217
}

java/org/apache/catalina/core/OpenSSLLifecycleListener.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,29 @@ public static boolean isAvailable() {
6767
return OpenSSLStatus.isAvailable();
6868
}
6969

70+
/**
71+
* Get the installed OpenSSL version string (via FFM), if available.
72+
*
73+
* @return the OpenSSL version string (e.g., "OpenSSL 3.2.6 30 Sep 2025"), or null if not available
74+
*/
75+
public static String getInstalledOpenSslVersion() {
76+
if (!isAvailable()) {
77+
return null;
78+
}
79+
80+
if (JreCompat.isJre22Available()) {
81+
try {
82+
Class<?> openSSLLibraryClass =
83+
Class.forName("org.apache.tomcat.util.net.openssl.panama.OpenSSLLibrary");
84+
return (String) openSSLLibraryClass.getMethod("getVersionString").invoke(null);
85+
} catch (Throwable t) {
86+
Throwable throwable = ExceptionUtils.unwrapInvocationTargetException(t);
87+
ExceptionUtils.handleThrowable(throwable);
88+
}
89+
}
90+
return null;
91+
}
92+
7093
public OpenSSLLifecycleListener() {
7194
OpenSSLStatus.setInstanceCreated(true);
7295
}

java/org/apache/catalina/util/ServerInfo.java

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@
1717
package org.apache.catalina.util;
1818

1919

20+
import java.io.File;
2021
import java.io.InputStream;
22+
import java.util.ArrayList;
23+
import java.util.List;
2124
import java.util.Properties;
25+
import java.util.jar.JarFile;
26+
import java.util.jar.Manifest;
2227

2328
import org.apache.tomcat.util.ExceptionUtils;
2429

@@ -121,6 +126,10 @@ public static String getServerNumber() {
121126
}
122127

123128
public static void main(String[] args) {
129+
// Suppress INFO logging from library initialization
130+
java.util.logging.Logger.getLogger("org.apache.tomcat.util.net.openssl.panama").setLevel(java.util.logging.Level.WARNING);
131+
java.util.logging.Logger.getLogger("org.apache.catalina.core").setLevel(java.util.logging.Level.WARNING);
132+
124133
System.out.println("Server version: " + getServerInfo());
125134
System.out.println("Server built: " + getServerBuilt());
126135
System.out.println("Server number: " + getServerNumber());
@@ -129,6 +138,212 @@ public static void main(String[] args) {
129138
System.out.println("Architecture: " + System.getProperty("os.arch"));
130139
System.out.println("JVM Version: " + System.getProperty("java.runtime.version"));
131140
System.out.println("JVM Vendor: " + System.getProperty("java.vm.vendor"));
141+
142+
// Get CATALINA_HOME for library scanning (already displayed in catalina script output preface)
143+
String catalinaHome = System.getProperty("catalina.home");
144+
145+
// Display APR/Tomcat Native information if available
146+
boolean aprLoaded = false;
147+
try {
148+
// Try to initialize APR by creating an instance and calling isAprAvailable()
149+
// Creating an instance sets the instance flag which allows initialization
150+
Class<?> aprLifecycleListenerClass = Class.forName("org.apache.catalina.core.AprLifecycleListener");
151+
aprLifecycleListenerClass.getConstructor().newInstance();
152+
Boolean aprAvailable = (Boolean) aprLifecycleListenerClass.getMethod("isAprAvailable").invoke(null);
153+
if (aprAvailable != null && aprAvailable.booleanValue()) {
154+
// APR is available, get version information using public methods
155+
String tcnVersion = (String) aprLifecycleListenerClass.getMethod("getInstalledTcnVersion").invoke(null);
156+
String aprVersion = (String) aprLifecycleListenerClass.getMethod("getInstalledAprVersion").invoke(null);
157+
158+
System.out.println("APR loaded: true");
159+
System.out.println("APR Version: " + aprVersion);
160+
System.out.println("Tomcat Native: " + tcnVersion);
161+
aprLoaded = true;
162+
163+
// Check if installed version is older than recommended
164+
try {
165+
String warning = (String) aprLifecycleListenerClass.getMethod("getTcnVersionWarning").invoke(null);
166+
167+
if (warning != null) {
168+
System.out.println(" " + warning);
169+
}
170+
} catch (Exception e) {
171+
// Failed to check version - ignore
172+
}
173+
174+
// Display OpenSSL version if available
175+
try {
176+
String openSSLVersion = (String) aprLifecycleListenerClass.getMethod("getInstalledOpenSslVersion").invoke(null);
177+
178+
if (openSSLVersion != null && !openSSLVersion.isEmpty()) {
179+
System.out.println("OpenSSL (APR): " + openSSLVersion);
180+
}
181+
} catch (Exception e) {
182+
// SSL not initialized or not available
183+
}
184+
}
185+
} catch (ClassNotFoundException | NoClassDefFoundError e) {
186+
// APR/Tomcat Native classes not available on classpath
187+
} catch (Exception e) {
188+
// Error checking APR status
189+
}
190+
191+
if (!aprLoaded) {
192+
System.out.println("APR loaded: false");
193+
}
194+
195+
// Display FFM OpenSSL information if available
196+
try {
197+
// Try to initialize FFM OpenSSL by creating an instance and calling isAvailable()
198+
// Creating an instance sets the instance flag which allows initialization
199+
Class<?> openSSLLifecycleListenerClass = Class.forName("org.apache.catalina.core.OpenSSLLifecycleListener");
200+
openSSLLifecycleListenerClass.getConstructor().newInstance();
201+
Boolean ffmAvailable = (Boolean) openSSLLifecycleListenerClass.getMethod("isAvailable").invoke(null);
202+
203+
if (ffmAvailable != null && ffmAvailable.booleanValue()) {
204+
// FFM OpenSSL is available, get version information using public method
205+
String versionString = (String) openSSLLifecycleListenerClass.getMethod("getInstalledOpenSslVersion").invoke(null);
206+
207+
if (versionString != null && !versionString.isEmpty()) {
208+
System.out.println("OpenSSL (FFM): " + versionString);
209+
}
210+
}
211+
} catch (ClassNotFoundException | NoClassDefFoundError e) {
212+
// FFM OpenSSL classes not available on classpath
213+
} catch (Exception e) {
214+
// Error checking FFM OpenSSL status
215+
}
216+
217+
// Display third-party libraries in CATALINA_HOME/lib
218+
if (catalinaHome != null) {
219+
File libDir = new File(catalinaHome, "lib");
220+
if (libDir.exists() && libDir.isDirectory()) {
221+
File[] allJars = libDir.listFiles((dir, name) -> name.endsWith(".jar"));
222+
223+
if (allJars != null && allJars.length > 0) {
224+
// First pass: collect third-party JARs and find longest name
225+
List<File> thirdPartyJars = new ArrayList<>();
226+
int maxNameLength = 0;
227+
for (File jar : allJars) {
228+
if (!isTomcatCoreJar(jar)) {
229+
thirdPartyJars.add(jar);
230+
maxNameLength = Math.max(maxNameLength, jar.getName().length());
231+
}
232+
}
233+
234+
// Second pass: print with aligned formatting
235+
if (!thirdPartyJars.isEmpty()) {
236+
System.out.println();
237+
System.out.println("Third-party libraries:");
238+
for (File jar : thirdPartyJars) {
239+
String version = getJarVersion(jar);
240+
String jarName = jar.getName();
241+
// Colon right after name, then pad to align version numbers
242+
String nameWithColon = jarName + ":";
243+
String paddedName = String.format("%-" + (maxNameLength + 1) + "s", nameWithColon);
244+
if (version != null) {
245+
System.out.println(" " + paddedName + " " + version);
246+
} else {
247+
System.out.println(" " + paddedName + " (unknown)");
248+
}
249+
}
250+
}
251+
}
252+
}
253+
}
254+
}
255+
256+
private static boolean isTomcatCoreJar(File jarFile) {
257+
try (JarFile jar = new JarFile(jarFile)) {
258+
Manifest manifest = jar.getManifest();
259+
260+
if (manifest != null) {
261+
// Check Bundle-SymbolicName to identify Tomcat core JARs
262+
String bundleName = manifest.getMainAttributes().getValue("Bundle-SymbolicName");
263+
if (bundleName != null) {
264+
// Tomcat core JARs have Bundle-SymbolicName starting with org.apache.tomcat,
265+
// org.apache.catalina, or jakarta.
266+
if (bundleName.startsWith("org.apache.tomcat") ||
267+
bundleName.startsWith("org.apache.catalina") ||
268+
bundleName.startsWith("jakarta.")) {
269+
return true;
270+
}
271+
}
272+
273+
// Fallback: Check Implementation-Vendor and Implementation-Title
274+
String implVendor = manifest.getMainAttributes().getValue("Implementation-Vendor");
275+
String implTitle = manifest.getMainAttributes().getValue("Implementation-Title");
276+
277+
if ("Apache Software Foundation".equals(implVendor) && "Apache Tomcat".equals(implTitle)) {
278+
return true;
279+
}
280+
}
281+
} catch (Exception e) {
282+
// Ignore errors reading JAR manifest
283+
}
284+
285+
return false;
286+
}
287+
288+
private static String getJarVersion(File jarFile) {
289+
// First try manifest attributes
290+
try (JarFile jar = new JarFile(jarFile)) {
291+
Manifest manifest = jar.getManifest();
292+
293+
if (manifest != null) {
294+
// Try different common version attributes
295+
String[] versionAttrs = {"Bundle-Version", "Implementation-Version", "Specification-Version"};
296+
for (String attr : versionAttrs) {
297+
String version = manifest.getMainAttributes().getValue(attr);
298+
if (version != null) {
299+
return version;
300+
}
301+
}
302+
}
303+
} catch (Exception e) {
304+
// Ignore errors reading JAR manifest
305+
}
306+
307+
// Fallback: try to parse version from filename
308+
return parseVersionFromFilename(jarFile.getName());
309+
}
310+
311+
/**
312+
* Attempt to extract a version number from a JAR filename.
313+
* Common patterns include:
314+
* - name-version.jar (e.g., commons-logging-1.2.jar)
315+
* - name_version.jar (e.g., library_2.3.4.jar)
316+
* - name-version-SNAPSHOT.jar (e.g., mylib-1.0.0-SNAPSHOT.jar)
317+
*
318+
* @param filename the JAR filename
319+
* @return the extracted version string, or null if no version pattern is found
320+
*/
321+
private static String parseVersionFromFilename(String filename) {
322+
if (filename == null || !filename.endsWith(".jar")) {
323+
return null;
324+
}
325+
326+
// Remove .jar extension
327+
String nameWithoutExt = filename.substring(0, filename.length() - 4);
328+
329+
// Try to find version pattern by looking for the first separator followed by a digit
330+
// Search from right to left to find the start of the version string
331+
String[] separators = {"-", "_"};
332+
for (String sep : separators) {
333+
// Find all occurrences of the separator
334+
int index = nameWithoutExt.indexOf(sep);
335+
while (index >= 0 && index < nameWithoutExt.length() - 1) {
336+
String candidate = nameWithoutExt.substring(index + 1);
337+
// Check if this looks like a version number (starts with digit)
338+
if (!candidate.isEmpty() && Character.isDigit(candidate.charAt(0))) {
339+
return candidate;
340+
}
341+
// Move to next separator
342+
index = nameWithoutExt.indexOf(sep, index + 1);
343+
}
344+
}
345+
346+
return null;
132347
}
133348

134349
}

0 commit comments

Comments
 (0)