mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -05:00
#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:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class DuplicateLibraryDetectorTest
|
||||
{
|
||||
private static final String LIB = "/opt/sdr-trunk/bin/../lib/";
|
||||
|
||||
/**
|
||||
* Jar names from a clean release image.
|
||||
*/
|
||||
private static final List<String> CLEAN = List.of("JLargeArrays-1.7.jar", "JTransforms-3.2.jar", "api-v2.1.1.jar",
|
||||
"commons-lang3-3.21.0.jar", "guava-33.8.0-jre.jar", "jackson-annotations-2.23.jar",
|
||||
"jackson-core-2.23.0.jar", "java-lame-v3.98.4.jar", "javafx-swt.jar", "jhackrf-2026.01.3.1.jar",
|
||||
"jiconfont-1.0.0.jar", "jiconfont-font_awesome-4.7.0.1.jar", "jiconfont-javafx-1.0.0.jar",
|
||||
"jiconfont-swing-1.0.1.jar", "jrt-fs.jar", "jrtl-1.4.0.3.jar", "libresample4j-master.jar",
|
||||
"listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar", "logback-classic-1.6.6.jar",
|
||||
"logback-core-1.6.6.jar", "sdr-trunk-nightly.jar");
|
||||
|
||||
private static List<String> paths(List<String> names)
|
||||
{
|
||||
return names.stream().map(n -> LIB + n).toList();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cleanReleaseHasNoDuplicates()
|
||||
{
|
||||
assertTrue(DuplicateLibraryDetector.findDuplicates(paths(CLEAN)).isEmpty());
|
||||
assertNull(DuplicateLibraryDetector.getFolder(Map.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void newReleaseUnzippedOverOldReleaseIsDetected()
|
||||
{
|
||||
List<String> names = new java.util.ArrayList<>(CLEAN);
|
||||
names.addAll(List.of("api-v2.1.0.jar", "guava-33.6.0-jre.jar", "jrtl-1.4.0.2.jar", "sdr-trunk-0.6.1.jar"));
|
||||
|
||||
Map<String,List<String>> duplicates = DuplicateLibraryDetector.findDuplicates(paths(names));
|
||||
|
||||
assertEquals(List.of("api", "guava", "jrtl", "sdr-trunk"), List.copyOf(duplicates.keySet()));
|
||||
assertEquals(List.of(LIB + "jrtl-1.4.0.2.jar", LIB + "jrtl-1.4.0.3.jar"), duplicates.get("jrtl"));
|
||||
assertTrue(DuplicateLibraryDetector.describe(duplicates).contains("jrtl: jrtl-1.4.0.2.jar, jrtl-1.4.0.3.jar"));
|
||||
//Windows resolves a rooted path without a drive letter against the current drive, e.g. C:\opt\sdr-trunk\lib
|
||||
assertEquals(Path.of("/opt/sdr-trunk/lib").toAbsolutePath(), DuplicateLibraryDetector.getFolder(duplicates));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameJarListedTwiceIsNotADuplicate()
|
||||
{
|
||||
assertTrue(DuplicateLibraryDetector.findDuplicates(List.of(LIB + "jrtl-1.4.0.3.jar", LIB + "jrtl-1.4.0.3.jar",
|
||||
"", "/some/classes/dir")).isEmpty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user