| /******************************************************************************* |
| * Copyright (c) 2016 Ericsson. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| *******************************************************************************/ |
| |
| package org.eclipse.cdt.llvm.dsf.lldb.core.internal.launching; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.text.MessageFormat; |
| import java.util.HashSet; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.cdt.core.parser.util.StringUtil; |
| import org.eclipse.cdt.dsf.gdb.launching.GdbLaunch; |
| import org.eclipse.cdt.llvm.dsf.lldb.core.ILLDBDebugPreferenceConstants; |
| import org.eclipse.cdt.llvm.dsf.lldb.core.ILLDBLaunchConfigurationConstants; |
| import org.eclipse.cdt.llvm.dsf.lldb.core.internal.ILLDBConstants; |
| import org.eclipse.cdt.llvm.dsf.lldb.core.internal.LLDBCorePlugin; |
| import org.eclipse.cdt.llvm.dsf.lldb.core.internal.LLDBTrait; |
| import org.eclipse.cdt.utils.CommandLineUtil; |
| import org.eclipse.cdt.utils.spawner.ProcessFactory; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.core.variables.VariablesPlugin; |
| import org.eclipse.debug.core.DebugException; |
| import org.eclipse.debug.core.ILaunchConfiguration; |
| import org.eclipse.debug.core.model.ISourceLocator; |
| |
| /** |
| * LLDB specific launch. It mostly deals with setting up the paths correctly. |
| */ |
| public class LLDBLaunch extends GdbLaunch { |
| |
| private static final String XCODE_HINT = "(Xcode 7.3.1)"; //$NON-NLS-1$ |
| private static final IntegerTuple LLDB_MINIMUM_REVISION = new IntegerTuple(350, 0, 21, 9); |
| private static final IntegerTuple LLDB_MINIMUM_VERSION = new IntegerTuple(3, 8, 0); |
| private static final Pattern LLDB_VERSION_PATTERN = Pattern.compile("lldb\\s*version\\s*(\\d+)\\.(\\d+)\\.(\\d+).*", //$NON-NLS-1$ |
| Pattern.DOTALL); //; |
| private static final Pattern LLDB_REVISION_PATTERN = Pattern.compile("lldb-(\\d+)\\.(\\d+)\\.(\\d+)(\\.(\\d)+)?.*", //$NON-NLS-1$ |
| Pattern.DOTALL); |
| |
| private Optional<IntegerTuple> fLldbVersion; |
| private Optional<IntegerTuple> fLldbRevision; |
| private Set<LLDBTrait> fTraits = new HashSet<>(); |
| |
| /** |
| * Constructs a launch. |
| * |
| * @param launchConfiguration |
| * the launch configuration |
| * @param mode |
| * the launch mode, i.e., debug, profile, etc. |
| * @param locator |
| */ |
| public LLDBLaunch(ILaunchConfiguration launchConfiguration, String mode, ISourceLocator locator) { |
| super(launchConfiguration, mode, locator); |
| } |
| |
| /* |
| * TODO: GdbLaunch.getGDBPath() and setGDBPath() should reference each other |
| * in the javadoc to make sure extenders override both. |
| */ |
| @Override |
| public IPath getGDBPath() { |
| String lldbPath = getAttribute(ILLDBLaunchConfigurationConstants.ATTR_DEBUG_NAME); |
| if (lldbPath != null) { |
| return new Path(lldbPath); |
| } |
| |
| return getLLDBPath(getLaunchConfiguration()); |
| } |
| |
| @Override |
| public void setGDBPath(String path) { |
| setAttribute(ILLDBLaunchConfigurationConstants.ATTR_DEBUG_NAME, path); |
| } |
| |
| /** |
| * Get the LLDB path based on a launch configuration. |
| * |
| * @param configuration |
| * the launch configuration. |
| * @return the LLDB path |
| */ |
| public static IPath getLLDBPath(ILaunchConfiguration configuration) { |
| String defaultLLdbCommand = getDefaultLLDBPath(); |
| |
| IPath retVal = new Path(defaultLLdbCommand); |
| try { |
| String lldbPath = configuration.getAttribute(ILLDBLaunchConfigurationConstants.ATTR_DEBUG_NAME, |
| defaultLLdbCommand); |
| lldbPath = VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(lldbPath, |
| false); |
| retVal = new Path(lldbPath); |
| } catch (CoreException e) { |
| LLDBCorePlugin.getDefault().getLog().log(e.getStatus()); |
| } |
| return retVal; |
| } |
| |
| @Override |
| protected String getDefaultGDBPath() { |
| return getDefaultLLDBPath(); |
| } |
| |
| @Override |
| public String getGDBVersion() throws CoreException { |
| String gdbVersion = super.getGDBVersion(); |
| computeLLDBVersions(); |
| if (fLldbRevision.isPresent()) { |
| if (fLldbRevision.get().compareTo(LLDB_MINIMUM_REVISION) < 0) { |
| throw new DebugException( |
| LLDBCorePlugin.createStatus(MessageFormat.format(Messages.LLDBLaunch_minimum_version_error, |
| fLldbRevision.get(), LLDB_MINIMUM_REVISION, XCODE_HINT))); |
| } |
| } else if (fLldbVersion.isPresent()) { |
| if (fLldbVersion.get().compareTo(LLDB_MINIMUM_VERSION) < 0) { |
| throw new DebugException(LLDBCorePlugin.createStatus(MessageFormat.format( |
| Messages.LLDBLaunch_minimum_version_error, fLldbVersion.get(), LLDB_MINIMUM_VERSION, ""))); //$NON-NLS-1$ |
| } |
| } |
| |
| return gdbVersion; |
| } |
| |
| private void computeLLDBVersions() throws CoreException { |
| if (fLldbRevision != null || fLldbVersion != null) { |
| return; |
| } |
| |
| // Initialize to non-null here so that we don't try to retrieve the version and spawn a process repeatedly. |
| fLldbRevision = Optional.empty(); |
| fLldbVersion = Optional.empty(); |
| |
| // LLDB-MI always outputs the GDB version so try LLDB (non-MI) |
| // FIXME: There should be a better way to get the lldb version number |
| // from lldb-mi because "lldb" is not guaranteed to be next to lldb-mi. |
| // (Especially since Xcode doesn't include lldb-mi anymore). |
| IPath lldbMiPath = getGDBPath(); |
| String lastSegment = lldbMiPath.lastSegment(); |
| if (lastSegment.contains(ILLDBConstants.LLDB_MI_EXECUTABLE_NAME)) { |
| lastSegment = lastSegment.replace(ILLDBConstants.LLDB_MI_EXECUTABLE_NAME, |
| ILLDBConstants.LLDB_EXECUTABLE_NAME); |
| } |
| lldbMiPath = lldbMiPath.removeLastSegments(1).append(lastSegment); |
| |
| String cmd = lldbMiPath + " --version"; //$NON-NLS-1$ |
| |
| // Parse cmd to properly handle spaces and such things (bug 458499) |
| String[] args = CommandLineUtil.argumentsToArray(cmd); |
| |
| Process process = null; |
| Job timeoutJob = null; |
| try { |
| process = ProcessFactory.getFactory().exec(args, getLaunchEnvironment()); |
| |
| // Start a timeout job to make sure we don't get stuck waiting for |
| // an answer from a gdb that is hanging |
| // Bug 376203 |
| final Process finalProc = process; |
| timeoutJob = new Job("LLDB version timeout job") { //$NON-NLS-1$ |
| { |
| setSystem(true); |
| } |
| |
| @Override |
| protected IStatus run(IProgressMonitor arg) { |
| // Took too long. Kill the lldb process and |
| // let things clean up. |
| finalProc.destroy(); |
| return Status.OK_STATUS; |
| } |
| }; |
| timeoutJob.schedule(10000); |
| |
| String streamOutput = readStream(process.getInputStream()); |
| |
| fLldbVersion = getLLDBVersionFromText(streamOutput); |
| fLldbRevision = getLLDBRevisionFromText(streamOutput); |
| if (fLldbVersion.isEmpty() && fLldbRevision.isEmpty()) { |
| if (!streamOutput.isEmpty()) { |
| // We got some output but couldn't parse it. Make that |
| // output visible to the user in the error dialog. |
| Exception detailedException = new Exception("Unexpected output format: \n\n" + streamOutput); //$NON-NLS-1$ |
| throw new DebugException(LLDBCorePlugin.createStatus( |
| "Could not determine LLDB version using command: " + StringUtil.join(args, " "), //$NON-NLS-1$ //$NON-NLS-2$ |
| detailedException)); |
| } |
| } |
| computeTraits(); |
| } catch (IOException e) { |
| // Since we can't use lldb-mi for version checking, we try to use |
| // the lldb executable but it's possible that it's not there at all |
| // and that shouldn't prevent users to start debugging with lldb-mi. |
| // So here we log instead of throwing an exception and stopping the |
| // launch. |
| LLDBCorePlugin.log(new DebugException(new Status(IStatus.ERROR, LLDBCorePlugin.PLUGIN_ID, |
| DebugException.REQUEST_FAILED, "Error with command: " + StringUtil.join(args, " "), e))); //$NON-NLS-1$ //$NON-NLS-2$ |
| } finally { |
| // If we get here we are obviously not stuck reading the stream so |
| // we can cancel the timeout job. |
| // Note that it may already have executed, but that is not a |
| // problem. |
| if (timeoutJob != null) { |
| timeoutJob.cancel(); |
| } |
| |
| if (process != null) { |
| process.destroy(); |
| } |
| } |
| } |
| |
| private void computeTraits() { |
| // Here are some LLDB/Xcode version mappings |
| // 360.1.65 => Xcode 8.1.0 |
| // 360.1.70 => Xcode 8.2.1, 8.2.0 |
| // 370.0.37 => Xcode 8.3.0 |
| // 370.0.40 => Xcode 8.3.1 |
| // 902.0.79.7 => Xcode 9.4.1 |
| // 1000.11.37.1 => Xcode 10.0 |
| // 1100.0.28.19 => Xcode 11.1 (lldb-mi not included anymore) |
| // |
| // Note that a LLDB built from source on macOS can report the same |
| // Apple-style version even for different LLDB/Clang-style version |
| // For example, 3.9.1 and 4.0.0 both report 360.99.0, how |
| // inconvenient! But this will only affect people building it from |
| // source, not LLDB included in Xcode. |
| |
| if (fLldbVersion.isPresent() && fLldbVersion.get().compareTo(new IntegerTuple(4, 0, 0)) < 0 |
| || fLldbRevision.isPresent() && fLldbRevision.get().compareTo(new IntegerTuple(370, 0, 37)) < 0) { |
| fTraits.add(LLDBTrait.BROKEN_BREAKPOINT_INSERT_FULL_PATH_LLVM_BUG_28709); |
| } |
| |
| if (fLldbVersion.isPresent() && fLldbVersion.get().compareTo(new IntegerTuple(8, 0, 0)) < 0 |
| || fLldbRevision.isPresent()) { |
| fTraits.add(LLDBTrait.MISSING_GDB_SET_BREAKPOINT_PENDING); |
| } |
| } |
| |
| /** |
| * Read from the specified stream and return what was read. |
| * |
| * @param stream |
| * The input stream to be used to read the data. This method will |
| * close the stream. |
| * @return The data read from the stream |
| * @throws IOException |
| * If an IOException happens when reading the stream |
| */ |
| private static String readStream(InputStream stream) throws IOException { |
| StringBuilder cmdOutput = new StringBuilder(200); |
| try { |
| Reader r = new InputStreamReader(stream); |
| BufferedReader reader = new BufferedReader(r); |
| |
| String line; |
| while ((line = reader.readLine()) != null) { |
| cmdOutput.append(line); |
| cmdOutput.append('\n'); |
| } |
| return cmdOutput.toString(); |
| } finally { |
| // Cleanup to avoid leaking pipes |
| // Bug 345164 |
| if (stream != null) { |
| try { |
| stream.close(); |
| } catch (IOException e) { |
| } |
| } |
| } |
| } |
| |
| private static class IntegerTuple implements Comparable<IntegerTuple> { |
| private Integer[] fIntegers; |
| |
| private IntegerTuple(Integer... integers) { |
| fIntegers = integers; |
| } |
| |
| @Override |
| public int compareTo(IntegerTuple o) { |
| for (int i = 0; i < fIntegers.length; i++) { |
| if (i >= o.fIntegers.length) { |
| // All numbers are the same up to now but the other tuple |
| // has less |
| return 1; |
| } |
| |
| int compareTo = fIntegers[i].compareTo(o.fIntegers[i]); |
| if (compareTo != 0) { |
| return compareTo; |
| } |
| } |
| |
| // All numbers are the same up to now but this tuple has less than |
| // the other |
| if (fIntegers.length < o.fIntegers.length) { |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < fIntegers.length; i++) { |
| sb.append(fIntegers[i]); |
| if (i != fIntegers.length - 1) { |
| sb.append("."); //$NON-NLS-1$ |
| } |
| } |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * This depends on the SVN revision, for example 350.0.21.9 |
| * |
| * @param versionOutput |
| * output text from "lldb --version" command . |
| * @return String representation of revision of lldb such as "350.0.21.9" on |
| * success; null otherwise. |
| */ |
| private static Optional<IntegerTuple> getLLDBRevisionFromText(String versionOutput) { |
| // These are the LLDB version patterns I have seen up to now |
| // Apple Xcode 7.3.1: lldb-350.0.21.9 |
| // LLVM build: lldb-360.99.0 |
| |
| Matcher matcher = LLDB_REVISION_PATTERN.matcher(versionOutput); |
| if (!matcher.matches()) { |
| return Optional.empty(); |
| } |
| |
| try { |
| Integer major = Integer.valueOf(matcher.group(1)); |
| Integer minor = Integer.valueOf(matcher.group(2)); |
| Integer micro = Integer.valueOf(matcher.group(3)); |
| String patchGroup = matcher.group(5); |
| if (patchGroup != null) { |
| Integer patch = Integer.valueOf(patchGroup); |
| return Optional.of(new IntegerTuple(major, minor, micro, patch)); |
| } else { |
| return Optional.of(new IntegerTuple(major, minor, micro)); |
| } |
| } catch (NumberFormatException e) { |
| LLDBCorePlugin.log(e); |
| } |
| return Optional.empty(); |
| } |
| |
| /** |
| * Returns Clang-style/LLVM version, for example 3.9.0 |
| * |
| * @param versionOutput |
| * output text from "lldb --version" command . |
| * @return String representation of version of lldb such as "3.9.0" on |
| * success; null otherwise. |
| */ |
| private static Optional<IntegerTuple> getLLDBVersionFromText(String versionOutput) { |
| // These are the LLDB version patterns I have seen up to now |
| // Ubuntu 14.04: lldb version 3.6.0 ( revision ) |
| // Ubuntu 14.04: lldb version 3.8.0 ( revision ) |
| |
| Matcher matcher = LLDB_VERSION_PATTERN.matcher(versionOutput); |
| if (!matcher.find()) { |
| return Optional.empty(); |
| } |
| |
| try { |
| Integer major = Integer.valueOf(matcher.group(1)); |
| Integer minor = Integer.valueOf(matcher.group(2)); |
| Integer micro = Integer.valueOf(matcher.group(3)); |
| IntegerTuple version = new IntegerTuple(major, minor, micro); |
| return Optional.of(version); |
| } catch (NumberFormatException e) { |
| LLDBCorePlugin.log(e); |
| } |
| return Optional.empty(); |
| } |
| |
| private static String getDefaultLLDBPath() { |
| return Platform.getPreferencesService().getString(LLDBCorePlugin.PLUGIN_ID, |
| ILLDBDebugPreferenceConstants.PREF_DEFAULT_LLDB_COMMAND, |
| ILLDBLaunchConfigurationConstants.DEBUGGER_DEBUG_NAME_DEFAULT, null); |
| } |
| |
| @Override |
| public String getGDBInitFile() throws CoreException { |
| // Not supported by LLDB-MI right now. There is also no MI command in |
| // GDB to source a file. We should look into adding this in GDB first. |
| return null; |
| } |
| |
| /** |
| * Returns whether or not the LLDB use by this launch has the given trait. |
| * |
| * @param trait |
| * the trait to check |
| * @return if the launch has this trait for the LLDB, false otherwise |
| */ |
| public boolean hasTrait(LLDBTrait trait) { |
| return fTraits.contains(trait); |
| } |
| } |