blob: 8b1b175e4ffca97a6018900bcd5e6b8ee9f9068f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017, 2019 Kichwa Coders Ltd. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.lsp4e.debug.launcher;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.ILaunchConfigurationDelegate;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.lsp4e.debug.DSPPlugin;
import org.eclipse.lsp4e.debug.debugmodel.DSPDebugTarget;
import org.eclipse.osgi.util.NLS;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
public class DSPLaunchDelegate implements ILaunchConfigurationDelegate {
/**
* Structured arguments for the
* {@link DSPLaunchDelegate#launch(DSPLaunchDelegateLaunchBuilder) method.
*
* Use this class to simplify calling the launch method and make the source code
* of consumers a little easier to read.
*/
public static class DSPLaunchDelegateLaunchBuilder {
ILaunchConfiguration configuration;
String mode;
ILaunch launch;
IProgressMonitor monitor;
boolean launchNotConnect;
String debugCmd;
List<String> debugCmdArgs;
boolean monitorDebugAdapter;
String server;
int port;
Map<String, Object> dspParameters;
private DSPLaunchDelegateLaunchBuilder(DSPLaunchDelegateLaunchBuilder other) {
this.configuration = other.configuration;
this.mode = other.mode;
this.launch = other.launch;
this.monitor = other.monitor;
this.launchNotConnect = other.launchNotConnect;
this.debugCmd = other.debugCmd;
this.debugCmdArgs = other.debugCmdArgs == null ? null : new ArrayList<>(other.debugCmdArgs);
this.monitorDebugAdapter = other.monitorDebugAdapter;
this.server = other.server;
this.port = other.port;
this.dspParameters = new HashMap<>(other.dspParameters);
}
/**
* Construct a builder using the normal arguments to
* {@link ILaunchConfigurationDelegate#launch(ILaunchConfiguration, String, ILaunch, IProgressMonitor)}
*
* @param configuration
* @param mode
* @param launch
* @param monitor
*/
public DSPLaunchDelegateLaunchBuilder(ILaunchConfiguration configuration, String mode, ILaunch launch,
IProgressMonitor monitor) {
this.configuration = configuration;
this.mode = mode;
this.launch = launch;
this.monitor = monitor;
}
public DSPLaunchDelegateLaunchBuilder setLaunchDebugAdapter(String debugCmd, List<String> debugCmdArgs) {
this.launchNotConnect = true;
this.debugCmd = debugCmd;
this.debugCmdArgs = debugCmdArgs;
return this;
}
public DSPLaunchDelegateLaunchBuilder setAttachDebugAdapter(String server, int port) {
this.launchNotConnect = false;
this.server = server;
this.port = port;
return this;
}
public DSPLaunchDelegateLaunchBuilder setMonitorDebugAdapter(boolean monitorDebugAdapter) {
this.monitorDebugAdapter = monitorDebugAdapter;
return this;
}
public DSPLaunchDelegateLaunchBuilder setDspParameters(Map<String, Object> dspParameters) {
this.dspParameters = dspParameters;
return this;
}
public ILaunchConfiguration getConfiguration() {
return configuration;
}
public String getMode() {
return mode;
}
public ILaunch getLaunch() {
return launch;
}
public IProgressMonitor getMonitor() {
return monitor;
}
public boolean isLaunchNotConnect() {
return launchNotConnect;
}
public String getDebugCmd() {
return debugCmd;
}
public List<String> getDebugCmdArgs() {
return debugCmdArgs;
}
public boolean isMonitorDebugAdapter() {
return monitorDebugAdapter;
}
public String getServer() {
return server;
}
public int getPort() {
return port;
}
public Map<String, Object> getDspParameters() {
return dspParameters;
}
@Override
public String toString() {
return "DSPLaunchDelegateLaunchBuilder [configuration=" + configuration + ", mode=" + mode + ", launch="
+ launch + ", monitor=" + monitor + ", launchNotConnect=" + launchNotConnect + ", debugCmd="
+ debugCmd + ", debugCmdArgs=" + debugCmdArgs + ", monitorDebugAdapter=" + monitorDebugAdapter
+ ", server=" + server + ", port=" + port + ", dspParameters=" + dspParameters + "]";
}
}
@Override
public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor)
throws CoreException {
DSPLaunchDelegateLaunchBuilder builder = new DSPLaunchDelegateLaunchBuilder(configuration, mode, launch,
monitor);
builder.launchNotConnect = DSPPlugin.DSP_MODE_LAUNCH
.equals(configuration.getAttribute(DSPPlugin.ATTR_DSP_MODE, DSPPlugin.DSP_MODE_LAUNCH));
builder.debugCmd = configuration.getAttribute(DSPPlugin.ATTR_DSP_CMD, (String) null);
builder.debugCmdArgs = configuration.getAttribute(DSPPlugin.ATTR_DSP_ARGS, (List<String>) null);
builder.monitorDebugAdapter = configuration.getAttribute(DSPPlugin.ATTR_DSP_MONITOR_DEBUG_ADAPTER, false);
builder.server = configuration.getAttribute(DSPPlugin.ATTR_DSP_SERVER_HOST, (String) null);
builder.port = configuration.getAttribute(DSPPlugin.ATTR_DSP_SERVER_PORT, 0);
String dspParametersJson = configuration.getAttribute(DSPPlugin.ATTR_DSP_PARAM, (String) null);
builder.dspParameters = jsonToMap(dspParametersJson);
launch(builder);
}
private Map<String, Object> jsonToMap(String dspParametersJson) {
Gson gson = new Gson();
Type type = new TypeToken<Map<String, Object>>() {
}.getType();
Map<String, Object> dspParameters = gson.fromJson(dspParametersJson, type);
if (dspParameters == null) {
dspParameters = new HashMap<>();
}
return dspParameters;
}
public void launch(DSPLaunchDelegateLaunchBuilder builder) throws CoreException {
// Make a copy so we can modify locally as needed.
builder = new DSPLaunchDelegateLaunchBuilder(builder);
SubMonitor subMonitor = SubMonitor.convert(builder.monitor, 100);
builder.dspParameters = new HashMap<>(builder.dspParameters);
boolean customDebugAdapter = builder.configuration.getAttribute(DSPPlugin.ATTR_CUSTOM_DEBUG_ADAPTER, false);
boolean customLaunchParameters = builder.configuration.getAttribute(DSPPlugin.ATTR_CUSTOM_LAUNCH_PARAMS, false);
if (customDebugAdapter) {
builder.launchNotConnect = DSPPlugin.DSP_MODE_LAUNCH
.equals(builder.configuration.getAttribute(DSPPlugin.ATTR_DSP_MODE, DSPPlugin.DSP_MODE_LAUNCH));
builder.debugCmd = builder.configuration.getAttribute(DSPPlugin.ATTR_DSP_CMD, (String) null);
builder.debugCmdArgs = builder.configuration.getAttribute(DSPPlugin.ATTR_DSP_ARGS, (List<String>) null);
builder.monitorDebugAdapter = builder.configuration.getAttribute(DSPPlugin.ATTR_DSP_MONITOR_DEBUG_ADAPTER,
false);
builder.server = builder.configuration.getAttribute(DSPPlugin.ATTR_DSP_SERVER_HOST, (String) null);
builder.port = builder.configuration.getAttribute(DSPPlugin.ATTR_DSP_SERVER_PORT, 0);
}
if (customLaunchParameters) {
String dspParametersJson = builder.configuration.getAttribute(DSPPlugin.ATTR_DSP_PARAM, (String) null);
Map<String, Object> customParams = jsonToMap(dspParametersJson);
builder.dspParameters.putAll(customParams);
}
// DSP supports run/debug as a simple flag to the debug server.
// See LaunchRequestArguments.noDebug
if (ILaunchManager.DEBUG_MODE.equals(builder.mode)) {
subMonitor.setTaskName("Starting debug session");
builder.dspParameters.put("noDebug", false);
} else if (ILaunchManager.RUN_MODE.equals(builder.mode)) {
subMonitor.setTaskName("Starting run session");
builder.dspParameters.put("noDebug", true);
} else {
abort(NLS.bind("Unsupported launch mode '{0}'.", builder.mode), null);
}
Runnable cleanup;
InputStream inputStream;
OutputStream outputStream;
try {
if (builder.launchNotConnect) {
List<String> command = new ArrayList<>();
if (builder.debugCmd == null) {
abort("Debug command unspecified.", null); //$NON-NLS-1$
}
command.add(builder.debugCmd);
if (builder.debugCmdArgs != null && !builder.debugCmdArgs.isEmpty()) {
command.addAll(builder.debugCmdArgs);
}
ProcessBuilder processBuilder = new ProcessBuilder(command);
subMonitor
.subTask(NLS.bind("Launching debug adapter: {0}", "\"" + String.join("\" \"", command) + "\""));
Process debugAdapterProcess = processBuilder.start();
if (builder.monitorDebugAdapter) {
// Uglish workaround: should instead have a dedicated ProcessFactory reading
// process attribute rather than launch one
String initialCaptureOutput = builder.launch.getAttribute(DebugPlugin.ATTR_CAPTURE_OUTPUT);
builder.launch.setAttribute(DebugPlugin.ATTR_CAPTURE_OUTPUT, Boolean.toString(true));
IProcess debugAdapterIProcess = DebugPlugin.newProcess(builder.launch, debugAdapterProcess,
"Debug Adapter");
builder.launch.setAttribute(DebugPlugin.ATTR_CAPTURE_OUTPUT, initialCaptureOutput);
List<Byte> bytes = Collections.synchronizedList(new LinkedList<>());
inputStream = new InputStream() {
@Override
public int read() throws IOException {
while (debugAdapterProcess.isAlive()) {
if (!bytes.isEmpty()) {
return bytes.remove(0);
} else {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
DSPPlugin.logError(e);
}
}
}
return -1;
}
};
DSPLaunchDelegateLaunchBuilder finalBuilder = builder;
debugAdapterIProcess.getStreamsProxy().getOutputStreamMonitor().addListener((text, monitor) -> {
try {
for (byte b : text
.getBytes(finalBuilder.launch.getAttribute(DebugPlugin.ATTR_CONSOLE_ENCODING))) {
bytes.add(Byte.valueOf(b));
}
} catch (IOException e) {
DSPPlugin.logError(e);
}
});
outputStream = new OutputStream() {
@Override
public void write(int b) throws IOException {
debugAdapterIProcess.getStreamsProxy().write(new String(new byte[] { (byte) b }));
}
};
cleanup = () -> {
try {
debugAdapterIProcess.terminate();
debugAdapterProcess.destroy();
} catch (DebugException e) {
DSPPlugin.logError(e);
}
};
} else {
inputStream = debugAdapterProcess.getInputStream();
outputStream = debugAdapterProcess.getOutputStream();
cleanup = debugAdapterProcess::destroy;
}
} else {
if (builder.server == null) {
abort("Debug server host unspecified.", null); //$NON-NLS-1$
}
if (builder.port < 1 || builder.port > 65535) {
abort("Debug server port unspecified or out of range 1-65535.", null); //$NON-NLS-1$
}
subMonitor.subTask(NLS.bind("Connecting to debug adapter: {0}:{1}", builder.server, builder.port));
Socket socket = new Socket(builder.server, builder.port);
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
cleanup = () -> {
try {
socket.close();
} catch (IOException e) {
// ignore inner resource exception
}
};
}
subMonitor.setWorkRemaining(80);
DSPDebugTarget target = new DSPDebugTarget(builder.launch, cleanup, inputStream, outputStream,
builder.dspParameters);
target.initialize(subMonitor.split(80));
builder.launch.addDebugTarget(target);
} catch (IOException | OperationCanceledException e1) {
abort("Failed to start debugging", e1);
} finally {
subMonitor.done();
}
}
/**
* Throws an exception with a new status containing the given message and
* optional exception.
*
* @param message error message
* @param e underlying exception
* @throws CoreException
*/
private void abort(String message, Throwable e) throws CoreException {
throw new CoreException(new Status(IStatus.ERROR, DSPPlugin.PLUGIN_ID, 0, message, e));
}
}