#2632 Detect on startup if multiple versions of any library are on the classpath and warn user to cleanup and reinstall (#2633)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-11-13 01:59:38 -05:00
committed by GitHub
parent 74dc32538f
commit 07b61be257
3 changed files with 259 additions and 1 deletions

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -60,6 +60,7 @@ import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.util.DuplicateLibraryDetector;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
@@ -81,6 +82,7 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.prefs.Preferences;
import javafx.application.Platform;
@@ -153,6 +155,17 @@ public class SDRTrunk implements Listener<TunerEvent>
mApplicationLog = new ApplicationLog(mUserPreferences);
mApplicationLog.start();
Map<String,List<String>> duplicateLibraries = DuplicateLibraryDetector.findDuplicates();
if(!duplicateLibraries.isEmpty())
{
mLog.warn("More than one version of these libraries is in the library folder [" +
DuplicateLibraryDetector.getFolder(duplicateLibraries) + "]. This happens when a new version of " +
"sdrtrunk is unzipped over an older one, and it can load the older library code. Delete the " +
"sdrtrunk folder and unzip the release into an empty folder.\n" +
DuplicateLibraryDetector.describe(duplicateLibraries));
}
mResourceMonitor = new ResourceMonitor(mUserPreferences);
ThreadPool.logSettings();
@@ -253,6 +266,11 @@ public class SDRTrunk implements Listener<TunerEvent>
{
updateTitle(tuner.getPreferredName());
}
if(!duplicateLibraries.isEmpty())
{
showDuplicateLibrariesDialog(duplicateLibraries);
}
}
if(calibrating && !GraphicsEnvironment.isHeadless())
@@ -909,6 +927,28 @@ public class SDRTrunk implements Listener<TunerEvent>
}
/**
* Warns the user that the library folder holds more than one version of some libraries, which happens when a new
* release is unzipped over an older one. The launch scripts put every jar in the library folder on the class
* path, so the older library code can load instead of the newer code.
* @param duplicates library name to jar paths, from DuplicateLibraryDetector
*/
private void showDuplicateLibrariesDialog(Map<String,List<String>> duplicates)
{
String message = "<html><body style='width: 450px'>" +
"<p>The sdrtrunk library folder has more than one version of these libraries:</p><br>" +
"<pre>" + DuplicateLibraryDetector.describe(duplicates) + "</pre>" +
"<p>This happens when a new version of sdrtrunk is unzipped over an older one. sdrtrunk can load " +
"the older library code, which can cause crashes or errors that the newer version fixed.</p><br>" +
"<p>Close sdrtrunk, delete the sdrtrunk folder that contains this library folder and unzip the " +
"release into a new, empty folder:</p><br>" +
"<p><b>" + DuplicateLibraryDetector.getFolder(duplicates) + "</b></p><br>" +
"<p>Your playlist and settings are stored in your user folder and won't be affected.</p>" +
"</body></html>";
JOptionPane.showMessageDialog(mMainGui, message, "Duplicate Library Versions", JOptionPane.WARNING_MESSAGE);
}
/**
* Launch the application.
*/

View File

@@ -0,0 +1,140 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.util;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Detects more than one version of the same library jar on the class path.
*
* The release launch scripts use a 'lib/*' wildcard class path, so when a user unzips a new release over an older one,
* the stale jars from the older release stay on the class path next to the new ones. The JVM loads classes (and the
* tuner libraries extract their native libraries) from whichever jar sorts first, which can silently run an older
* library version, including native code with bugs that the newer version fixed.
*/
public class DuplicateLibraryDetector
{
/**
* Splits a jar file name into the library name and version. The version starts at the first dash that is
* followed by a digit, a 'v' and a digit, or 'nightly' (e.g. guava-33.8.0-jre, api-v2.1.1, sdr-trunk-nightly).
*/
private static final Pattern JAR_NAME = Pattern.compile("^(.+?)-(v?\\d.*|nightly)\\.jar$");
private DuplicateLibraryDetector()
{
}
/**
* Finds the libraries that have more than one version on this JVM's class path.
* @return map of library name to the sorted jar paths for that library, empty when there are no duplicates.
*/
public static Map<String,List<String>> findDuplicates()
{
String classPath = System.getProperty("java.class.path", "");
return findDuplicates(List.of(classPath.split(Pattern.quote(File.pathSeparator))));
}
/**
* Finds the libraries that have more than one version in the class path entries. Entries that are not jar files
* or that don't have a version in the file name are ignored.
* @param classPathEntries to check
* @return map of library name to the sorted jar paths for that library, empty when there are no duplicates.
*/
public static Map<String,List<String>> findDuplicates(List<String> classPathEntries)
{
Map<String,TreeSet<String>> versions = new TreeMap<>();
Map<String,TreeSet<String>> paths = new TreeMap<>();
for(String entry: classPathEntries)
{
if(entry == null || entry.isBlank())
{
continue;
}
Path fileName = Path.of(entry).getFileName();
if(fileName == null)
{
continue;
}
Matcher matcher = JAR_NAME.matcher(fileName.toString());
if(matcher.matches())
{
versions.computeIfAbsent(matcher.group(1), k -> new TreeSet<>()).add(matcher.group(2));
paths.computeIfAbsent(matcher.group(1), k -> new TreeSet<>()).add(entry);
}
}
Map<String,List<String>> duplicates = new TreeMap<>();
for(Map.Entry<String,TreeSet<String>> entry: versions.entrySet())
{
if(entry.getValue().size() > 1)
{
duplicates.put(entry.getKey(), new ArrayList<>(paths.get(entry.getKey())));
}
}
return duplicates;
}
/**
* Creates a description of the duplicate libraries for a log entry or a dialog.
* @param duplicates from findDuplicates()
* @return description, one line per library, listing the jar file names for each library.
*/
public static String describe(Map<String,List<String>> duplicates)
{
StringBuilder sb = new StringBuilder();
for(Map.Entry<String,List<String>> entry: duplicates.entrySet())
{
List<String> names = entry.getValue().stream().map(p -> Path.of(p).getFileName().toString()).toList();
sb.append(" ").append(entry.getKey()).append(": ").append(String.join(", ", names)).append("\n");
}
return sb.toString();
}
/**
* Folder that holds the duplicate jars, for telling the user which folder to replace.
* @param duplicates from findDuplicates()
* @return folder of the first duplicate jar, or null if there are no duplicates or the path has no parent.
*/
public static Path getFolder(Map<String,List<String>> duplicates)
{
if(duplicates.isEmpty())
{
return null;
}
return Path.of(duplicates.values().iterator().next().getFirst()).toAbsolutePath().normalize().getParent();
}
}