diff --git a/bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/OpenOcdVersionManager.java b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/OpenOcdVersionManager.java new file mode 100644 index 000000000..c9f0b62e9 --- /dev/null +++ b/bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/OpenOcdVersionManager.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import com.espressif.idf.core.logging.Logger; + +public class OpenOcdVersionManager +{ + private OpenOcdVersionManager() + { + /* This utility class should not be instantiated */ + } + + private static final Map versionCache = new ConcurrentHashMap<>(); + + private static final Pattern VERSION_PATTERN = Pattern.compile( + "(?i)(?:Open On-Chip Debugger\\s+|v)(?\\d+)\\.(?\\d+)(?:\\.(?\\d+))?(?:-[a-z0-9]+-(?\\d+))?"); //$NON-NLS-1$ + + public static class OpenOcdVersion + { + public final int major; + public final int minor; + public final int patch; + public final int buildDate; + + public OpenOcdVersion(int major, int minor, int patch, int buildDate) + { + this.major = major; + this.minor = minor; + this.patch = patch; + this.buildDate = buildDate; + } + + public boolean isAtLeast(int targetMajor, int targetMinor) + { + return isAtLeast(targetMajor, targetMinor, 0); + } + + public boolean isAtLeast(int targetMajor, int targetMinor, int targetPatch) + { + + if (this.major > targetMajor) + return true; + if (this.major == targetMajor && this.minor > targetMinor) + return true; + return (this.major == targetMajor && this.minor == targetMinor && this.patch >= targetPatch); + } + + public boolean isBuildDateAtLeast(int targetMajor, int targetMinor, int targetBuildDate) + { + // If strictly newer major/minor, we assume the feature exists + if (this.major > targetMajor) + return true; + if (this.major == targetMajor && this.minor > targetMinor) + return true; + + // If exactly the same major/minor, check the build date + if (this.major == targetMajor && this.minor == targetMinor) + { + return this.buildDate >= targetBuildDate; + } + return false; + } + + @Override + public String toString() + { + return major + "." + minor + "." + patch + " (Build: " + buildDate + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + } + } + + public static OpenOcdVersion getVersion(String executablePath) + { + if (executablePath == null || executablePath.isEmpty()) + { + return new OpenOcdVersion(0, 0, 0, 0); + } + return versionCache.computeIfAbsent(executablePath, OpenOcdVersionManager::fetchVersionFromProcess); + } + + private static OpenOcdVersion fetchVersionFromProcess(String executablePath) + { + try + { + ProcessBuilder pb = new ProcessBuilder(executablePath, "--version"); //$NON-NLS-1$ + pb.redirectErrorStream(true); + Process process = pb.start(); + + try + { + if (!process.waitFor(2, TimeUnit.SECONDS)) + { + return new OpenOcdVersion(0, 0, 0, 0); + } + + try (BufferedReader reader = process.inputReader()) + { + String output = reader.lines().collect(Collectors.joining("\n")); //$NON-NLS-1$ + return parseVersionString(output); + } + } finally + { + if (process.isAlive()) + { + process.destroyForcibly(); + } + } + } + catch (IOException e) + { + Logger.log("Failed to execute or parse OpenOCD version fallback for path: " + executablePath, e); //$NON-NLS-1$ + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + Logger.log("Thread was interrupted while waiting for OpenOCD process to exit.", e); //$NON-NLS-1$ + } + + return new OpenOcdVersion(0, 0, 0, 0); + } + + public static OpenOcdVersion parseVersionString(String output) + { + if (output == null || output.trim().isEmpty()) + { + return new OpenOcdVersion(0, 0, 0, 0); + } + + Matcher matcher = VERSION_PATTERN.matcher(output); + if (matcher.find()) + { + int major = Integer.parseInt(matcher.group("major")); //$NON-NLS-1$ + int minor = Integer.parseInt(matcher.group("minor")); //$NON-NLS-1$ + int patch = matcher.group("patch") != null ? Integer.parseInt(matcher.group("patch")) : 0; //$NON-NLS-1$ //$NON-NLS-2$ + int buildDate = matcher.group("build") != null ? Integer.parseInt(matcher.group("build")) : 0; //$NON-NLS-1$//$NON-NLS-2$ + + return new OpenOcdVersion(major, minor, patch, buildDate); + } + + return new OpenOcdVersion(0, 0, 0, 0); + } +} diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java index 6979b0b6c..a8cd9d3a5 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java @@ -14,13 +14,8 @@ package com.espressif.idf.debug.gdbjtag.openocd; -import java.io.BufferedReader; -import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.eclipse.cdt.core.settings.model.ICConfigurationDescription; import org.eclipse.cdt.debug.gdbjtag.core.IGDBJtagConstants; @@ -29,15 +24,17 @@ import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; -import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.embedcdt.core.EclipseUtils; import org.eclipse.embedcdt.core.StringUtils; import org.eclipse.embedcdt.debug.gdbjtag.core.DebugUtils; +import org.eclipse.launchbar.core.ILaunchBarManager; +import org.eclipse.launchbar.core.target.ILaunchTarget; +import com.espressif.idf.core.build.IDFLaunchConstants; +import com.espressif.idf.core.util.OpenOcdVersionManager; import com.espressif.idf.core.util.PortChecker; import com.espressif.idf.debug.gdbjtag.openocd.preferences.DefaultPreferences; import com.espressif.idf.launch.serial.util.ESPFlashUtil; @@ -114,6 +111,19 @@ public static String[] getGdbServerCommandLineArray(ILaunchConfiguration configu configurationWorkingCopy.setAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, port); configurationWorkingCopy.doSave(); + ILaunchTarget activeLaunchTarget = Activator.getService(ILaunchBarManager.class).getActiveLaunchTarget(); + if (activeLaunchTarget != null) + { + String openocdLoc = activeLaunchTarget.getAttribute(IDFLaunchConstants.OPENOCD_USB_LOCATION, + (String) null); + + if (openocdLoc != null && supportsAdapterUsbCommand(executable)) + { + lst.add("-c"); + lst.add(String.format("adapter usb location %s", openocdLoc)); + } + } + lst.add("-c"); //$NON-NLS-1$ lst.add(String.format(fmtGdbPort, port)); @@ -336,55 +346,13 @@ public static boolean getDoStartGdbClient(ILaunchConfiguration config) throws Co // ------------------------------------------------------------------------ - private static boolean useModernPortSyntax(String executablePath) + private static boolean supportsAdapterUsbCommand(String executablePath) { - if (executablePath == null || executablePath.isEmpty()) - { - return false; - } - Pattern outputPattern = Pattern.compile("(?:Open On-Chip Debugger |v)(?\\d+)\\.(?\\d+)"); //$NON-NLS-1$ - - try - { - ProcessBuilder pb = new ProcessBuilder(executablePath, "--version"); //$NON-NLS-1$ - pb.redirectErrorStream(true); - Process process = pb.start(); - - try - { - if (!process.waitFor(2, TimeUnit.SECONDS)) - { - return false; - } + return OpenOcdVersionManager.getVersion(executablePath).isBuildDateAtLeast(0, 12, 20260424); + } - try (BufferedReader reader = process.inputReader()) - { - return reader.lines().map(outputPattern::matcher).filter(Matcher::find).findFirst().map(matcher -> { - int major = Integer.parseInt(matcher.group("major")); //$NON-NLS-1$ - int minor = Integer.parseInt(matcher.group("minor")); //$NON-NLS-1$ - return major > 0 || (major == 0 && minor >= 12); - }).orElse(false); - } - } finally - { - if (process.isAlive()) - { - process.destroyForcibly(); - } - } - } - catch (IOException e) - { - Activator.log(new CoreException(new Status(IStatus.WARNING, Activator.PLUGIN_ID, - "Failed to execute or parse OpenOCD version fallback for path: " + executablePath, e))); //$NON-NLS-1$ - return false; - } - catch (InterruptedException e) - { - Thread.currentThread().interrupt(); - Activator.log(new CoreException(new Status(IStatus.WARNING, Activator.PLUGIN_ID, - "Thread was interrupted while waiting for OpenOCD process to exit.", e))); //$NON-NLS-1$ - return false; - } + private static boolean useModernPortSyntax(String executablePath) + { + return OpenOcdVersionManager.getVersion(executablePath).isAtLeast(0, 12); } } diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbServerBackend.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbServerBackend.java index b5798b0bc..66ebd4eb2 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbServerBackend.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbServerBackend.java @@ -36,6 +36,7 @@ import com.espressif.idf.core.IDFCorePlugin; import com.espressif.idf.core.build.IDFLaunchConstants; +import com.espressif.idf.core.util.OpenOcdVersionManager; import com.espressif.idf.debug.gdbjtag.openocd.Activator; import com.espressif.idf.debug.gdbjtag.openocd.Configuration; @@ -226,9 +227,12 @@ protected Process launchGdbServerProcess(String[] commandLineArray) throws CoreE if (activeLaunchTarget != null) { String openocdLoc = activeLaunchTarget.getAttribute(IDFLaunchConstants.OPENOCD_USB_LOCATION, (String) null); - if (openocdLoc != null) { - envMap.put(IDFLaunchConstants.OPENOCD_USB_LOCATION, openocdLoc); - } + if (openocdLoc != null && commandLineArray != null && commandLineArray.length > 0 + && !supportsAdapterUsbCommand(commandLineArray[0])) + { + envMap.put(IDFLaunchConstants.OPENOCD_USB_LOCATION, openocdLoc); + } + } // Convert back to envp @@ -239,4 +243,9 @@ protected Process launchGdbServerProcess(String[] commandLineArray) throws CoreE return DebugUtils.exec(commandLineArray, envList.toArray(new String[0]), dir); } + + private boolean supportsAdapterUsbCommand(String executablePath) + { + return OpenOcdVersionManager.getVersion(executablePath).isBuildDateAtLeast(0, 12, 20260424); + } } diff --git a/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/util/test/OpenOcdVersionManagerTest.java b/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/util/test/OpenOcdVersionManagerTest.java new file mode 100644 index 000000000..992d44424 --- /dev/null +++ b/tests/com.espressif.idf.core.test/src/com/espressif/idf/core/util/test/OpenOcdVersionManagerTest.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright 2026 Espressif Systems (Shanghai) PTE LTD. All rights reserved. + * Use is subject to license terms. + *******************************************************************************/ +package com.espressif.idf.core.util.test; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.espressif.idf.core.util.OpenOcdVersionManager; +import com.espressif.idf.core.util.OpenOcdVersionManager.OpenOcdVersion; + +class OpenOcdVersionManagerTest { + + // ======================================================================== + // Regex Parsing Tests + // ======================================================================== + + @Test + void testParseStandardVersion() { + String output = "Open On-Chip Debugger 0.11.0\nLicensed under GNU GPL v2..."; + OpenOcdVersion version = OpenOcdVersionManager.parseVersionString(output); + + Assertions.assertEquals(0, version.major); + Assertions.assertEquals(11, version.minor); + Assertions.assertEquals(0, version.patch); + Assertions.assertEquals(0, version.buildDate); + } + + @Test + void testParseVendorVersionWithBuildDate() { + String output = "Open On-Chip Debugger v0.12.0-esp32-20240228\nLicensed under..."; + OpenOcdVersion version = OpenOcdVersionManager.parseVersionString(output); + + Assertions.assertEquals(0, version.major); + Assertions.assertEquals(12, version.minor); + Assertions.assertEquals(0, version.patch); + Assertions.assertEquals(20240228, version.buildDate); + } + + @Test + void testParseAlternativeVendor() { + String output = "Open On-Chip Debugger 0.10.3-custom123-20230101"; + OpenOcdVersion version = OpenOcdVersionManager.parseVersionString(output); + + Assertions.assertEquals(0, version.major); + Assertions.assertEquals(10, version.minor); + Assertions.assertEquals(3, version.patch); + Assertions.assertEquals(20230101, version.buildDate); + } + + @Test + void testParseMissingPatchVersion() { + // The regex allows the patch version to be optional + String output = "Open On-Chip Debugger v1.2-esp32-20260424"; + OpenOcdVersion version = OpenOcdVersionManager.parseVersionString(output); + + Assertions.assertEquals(1, version.major); + Assertions.assertEquals(2, version.minor); + Assertions.assertEquals(0, version.patch); // Defaults to 0 + Assertions.assertEquals(20260424, version.buildDate); + } + + @Test + void testParseInvalidInput() { + String output = "Some random error string or command not found"; + OpenOcdVersion version = OpenOcdVersionManager.parseVersionString(output); + + Assertions.assertEquals(0, version.major); + Assertions.assertEquals(0, version.minor); + Assertions.assertEquals(0, version.patch); + Assertions.assertEquals(0, version.buildDate); + } + + @Test + void testParseNullAndEmptyInput() { + OpenOcdVersion nullVersion = OpenOcdVersionManager.parseVersionString(null); + Assertions.assertEquals(0, nullVersion.major); + + OpenOcdVersion emptyVersion = OpenOcdVersionManager.parseVersionString(" \n "); + Assertions.assertEquals(0, emptyVersion.major); + } + + // ======================================================================== + // Version Logic Tests: isAtLeast(major, minor, [patch]) + // ======================================================================== + + @Test + void testIsAtLeastMajorMinor() { + OpenOcdVersion version = new OpenOcdVersion(0, 11, 0, 0); + + Assertions.assertTrue(version.isAtLeast(0, 10)); // Newer minor + Assertions.assertTrue(version.isAtLeast(0, 11)); // Exact match + Assertions.assertFalse(version.isAtLeast(0, 12)); // Older minor + Assertions.assertFalse(version.isAtLeast(1, 0)); // Older major + } + + @Test + void testIsAtLeastWithPatch() { + OpenOcdVersion version = new OpenOcdVersion(0, 12, 2, 0); + + Assertions.assertTrue(version.isAtLeast(0, 12, 1)); // Newer patch + Assertions.assertTrue(version.isAtLeast(0, 12, 2)); // Exact patch match + Assertions.assertFalse(version.isAtLeast(0, 12, 3)); // Older patch + Assertions.assertTrue(version.isAtLeast(0, 11, 5)); // Newer minor overrides patch + } + + // ======================================================================== + // Version Logic Tests: isBuildDateAtLeast + // ======================================================================== + + @Test + void testIsBuildDateAtLeast_ExactVersionNewerDate() { + OpenOcdVersion version = new OpenOcdVersion(0, 12, 0, 20260424); + + Assertions.assertTrue(version.isBuildDateAtLeast(0, 12, 20230101), + "Should pass because 20260424 >= 20230101"); + } + + @Test + void testIsBuildDateAtLeast_ExactVersionOlderDate() { + OpenOcdVersion version = new OpenOcdVersion(0, 12, 0, 20230101); + + Assertions.assertFalse(version.isBuildDateAtLeast(0, 12, 20260424), + "Should fail because 20230101 < 20260424"); + } + + @Test + void testIsBuildDateAtLeast_ExactSameDate() { + OpenOcdVersion version = new OpenOcdVersion(0, 12, 0, 20260424); + + Assertions.assertTrue(version.isBuildDateAtLeast(0, 12, 20260424), + "Should pass because dates are exactly equal"); + } + + @Test + void testIsBuildDateAtLeast_StrictlyNewerBaseVersion() { + OpenOcdVersion version = new OpenOcdVersion(0, 13, 0, 20220101); + + Assertions.assertTrue(version.isBuildDateAtLeast(0, 12, 20260424), + "Should pass immediately because 0.13.0 > 0.12.0, build date is ignored"); + } + + @Test + void testIsBuildDateAtLeast_OlderBaseVersion() { + OpenOcdVersion version = new OpenOcdVersion(0, 11, 0, 20290101); + + Assertions.assertFalse(version.isBuildDateAtLeast(0, 12, 20260424), + "Should fail immediately because 0.11.0 < 0.12.0, despite newer build date"); + } + + // ======================================================================== + // API Null Safety Tests + // ======================================================================== + + @Test + void testGetVersionNullAndEmptyPaths() { + OpenOcdVersion nullPathVersion = OpenOcdVersionManager.getVersion(null); + Assertions.assertEquals(0, nullPathVersion.major); + + OpenOcdVersion emptyPathVersion = OpenOcdVersionManager.getVersion(""); + Assertions.assertEquals(0, emptyPathVersion.major); + } + + // ======================================================================== + // Model ToString Test + // ======================================================================== + + @Test + void testToString() { + OpenOcdVersion version = new OpenOcdVersion(0, 12, 1, 20240101); + Assertions.assertEquals("0.12.1 (Build: 20240101)", version.toString()); + } +}