This is an automated email from the ASF dual-hosted git repository. entl pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/netbeans.git
The following commit(s) were added to refs/heads/master by this push: new c47c7f2 Debugger attach in VSCode extension. (#5346) c47c7f2 is described below commit c47c7f22bb7adc97af6c428100487ab2f82688e3 Author: Martin Entlicher <martin.entlic...@oracle.com> AuthorDate: Thu May 27 18:16:49 2021 +0200 Debugger attach in VSCode extension. (#5346) --- .../lsp/server/debugging/NbProtocolServer.java | 14 +- .../debugging/attach/AttachConfigurations.java | 222 +++++++++++++++++++++ .../debugging/attach/NbAttachRequestHandler.java | 222 +++++++++++++++++++++ .../server/debugging/launch/NbDebugSession.java | 2 +- .../java/lsp/server/protocol/DebugConnector.java | 151 ++++++++++++++ .../modules/java/lsp/server/protocol/Server.java | 27 ++- .../lsp/server/protocol/WorkspaceServiceImpl.java | 15 +- .../java/lsp/server/protocol/ServerTest.java | 42 ++++ java/java.lsp.server/vscode/package.json | 39 +++- java/java.lsp.server/vscode/src/extension.ts | 61 +++--- java/java.lsp.server/vscode/src/protocol.ts | 7 + 11 files changed, 758 insertions(+), 44 deletions(-) diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java index 56f20df..6ae1497 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/NbProtocolServer.java @@ -19,19 +19,15 @@ package org.netbeans.modules.java.lsp.server.debugging; import java.io.File; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; -import java.net.URL; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang3.StringUtils; import org.eclipse.lsp4j.debug.Capabilities; @@ -41,7 +37,6 @@ import org.eclipse.lsp4j.debug.ContinueResponse; import org.eclipse.lsp4j.debug.DisconnectArguments; import org.eclipse.lsp4j.debug.EvaluateArguments; import org.eclipse.lsp4j.debug.EvaluateResponse; -import org.eclipse.lsp4j.debug.ExceptionBreakMode; import org.eclipse.lsp4j.debug.ExceptionBreakpointsFilter; import org.eclipse.lsp4j.debug.ExceptionInfoArguments; import org.eclipse.lsp4j.debug.ExceptionInfoResponse; @@ -79,10 +74,11 @@ import org.netbeans.api.debugger.jpda.ObjectVariable; import org.netbeans.api.debugger.jpda.Variable; import org.netbeans.modules.debugger.jpda.truffle.vars.TruffleVariable; import org.netbeans.modules.java.lsp.server.LspSession; +import org.netbeans.modules.java.lsp.server.debugging.breakpoints.NbBreakpointsRequestHandler; +import org.netbeans.modules.java.lsp.server.debugging.attach.NbAttachRequestHandler; import org.netbeans.modules.java.lsp.server.debugging.launch.NbDebugSession; import org.netbeans.modules.java.lsp.server.debugging.launch.NbDisconnectRequestHandler; import org.netbeans.modules.java.lsp.server.debugging.launch.NbLaunchRequestHandler; -import org.netbeans.modules.java.lsp.server.debugging.breakpoints.NbBreakpointsRequestHandler; import org.netbeans.modules.java.lsp.server.debugging.variables.NbVariablesRequestHandler; import org.netbeans.modules.java.lsp.server.debugging.utils.ErrorUtilities; import org.netbeans.modules.nativeimage.api.debug.EvaluateException; @@ -99,6 +95,7 @@ public final class NbProtocolServer implements IDebugProtocolServer, LspSession. private final DebugAdapterContext context; private final NbLaunchRequestHandler launchRequestHandler = new NbLaunchRequestHandler(); + private final NbAttachRequestHandler attachRequestHandler = new NbAttachRequestHandler(); private final NbDisconnectRequestHandler disconnectRequestHandler = new NbDisconnectRequestHandler(); private final NbBreakpointsRequestHandler breakpointsRequestHandler = new NbBreakpointsRequestHandler(); private final NbVariablesRequestHandler variablesRequestHandler = new NbVariablesRequestHandler(); @@ -172,6 +169,11 @@ public final class NbProtocolServer implements IDebugProtocolServer, LspSession. } @Override + public CompletableFuture<Void> attach(Map<String, Object> args) { + return attachRequestHandler.attach(args, context); + } + + @Override public CompletableFuture<Void> disconnect(DisconnectArguments args) { return disconnectRequestHandler.disconnect(args, context); } diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurations.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurations.java new file mode 100644 index 0000000..5a99511 --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/AttachConfigurations.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.lsp.server.debugging.attach; + +import com.sun.jdi.Bootstrap; +import com.sun.jdi.VirtualMachineManager; +import com.sun.jdi.connect.AttachingConnector; +import com.sun.jdi.connect.Connector; +import com.sun.tools.attach.AttachNotSupportedException; +import com.sun.tools.attach.VirtualMachine; +import com.sun.tools.attach.VirtualMachineDescriptor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.MessageType; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; + +import org.netbeans.modules.java.lsp.server.debugging.utils.ErrorUtilities; +import org.netbeans.modules.java.lsp.server.protocol.DebugConnector; +import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient; +import org.netbeans.modules.java.lsp.server.protocol.QuickPickItem; +import org.netbeans.modules.java.lsp.server.protocol.Server; +import org.netbeans.modules.java.lsp.server.protocol.ShowQuickPickParams; +import org.openide.util.NbBundle.Messages; +import org.openide.util.RequestProcessor; + +/** + * Debugger attach configurations provider. + * + * @author Martin Entlicher + */ +public final class AttachConfigurations { + + static final String NAME_ATTACH_PROCESS = "Attach to Process"; // NOI18N + static final String NAME_ATTACH_SOCKET = "Attach to Port"; // NOI18N + static final String NAME_ATTACH_SHMEM = "Attach to Shared Memory"; // NOI18N + static final String NAME_ATTACH_BY = "Attach by "; // NOI18N + + static final String CONNECTOR_PROCESS = "com.sun.jdi.ProcessAttach"; // NOI18N + static final String CONNECTOR_SOCKET = "com.sun.jdi.SocketAttach"; // NOI18N + static final String CONNECTOR_SHMEM = "com.sun.jdi.SharedMemoryAttach"; // NOI18N + + static final String PROCESS_ARG_PID = "processId"; // NOI18N + static final String SOCKET_ARG_HOST = "hostName"; // NOI18N + static final String SOCKET_ARG_PORT = "port"; // NOI18N + static final String SHMEM_ARG_NAME = "sharedMemoryName"; // NOI18N + + private static final RequestProcessor RP = new RequestProcessor(AttachConfigurations.class); + + private AttachConfigurations() {} + + public static CompletableFuture<Object> findConnectors() { + return CompletableFuture.supplyAsync(() -> { + return listAttachingConnectors(); + }, RP); + } + + @Messages({"LBL_ConnectorPort=port of the debuggee JVM", "LBL_ConnectorShmemName=shared memory name"}) + private static List<DebugConnector> listAttachingConnectors() { + VirtualMachineManager vmm = Bootstrap.virtualMachineManager (); + List<AttachingConnector> attachingConnectors = vmm.attachingConnectors(); + List<DebugConnector> connectors = new ArrayList<>(5); + String type = "java8+"; // NOI18N + for (AttachingConnector ac : attachingConnectors) { + String connectorName = ac.name(); + Map<String, Connector.Argument> defaultArguments = ac.defaultArguments(); + DebugConnector connector; + switch (connectorName) { + case CONNECTOR_PROCESS: + connector = new DebugConnector(NAME_ATTACH_PROCESS, type, + Collections.singletonList(PROCESS_ARG_PID), + Collections.singletonList("${command:" + Server.JAVA_FIND_DEBUG_PROCESS_TO_ATTACH + "}")); // NOI18N + break; + case CONNECTOR_SOCKET: { + String hostName = getArgumentOrDefault(defaultArguments.get("hostname"), "localhost"); // NOI18N + String port = getArgumentOrDefault(defaultArguments.get("port"), "<"+Bundle.LBL_ConnectorPort()+">"); // NOI18N + connector = new DebugConnector(NAME_ATTACH_SOCKET, type, + Arrays.asList(SOCKET_ARG_HOST, SOCKET_ARG_PORT), + Arrays.asList(hostName, port)); + break; + } + case CONNECTOR_SHMEM: { + String name = getArgumentOrDefault(defaultArguments.get("name"), "<"+Bundle.LBL_ConnectorShmemName()+">"); // NOI18N + connector = new DebugConnector(NAME_ATTACH_SHMEM, type, + Collections.singletonList(SHMEM_ARG_NAME), + Collections.singletonList(name)); + break; + } + default: { + List<String> names = new ArrayList<>(); + List<String> values = new ArrayList<>(); + for (Connector.Argument arg : defaultArguments.values()) { + if (arg.mustSpecify()) { + names.add(arg.name()); + String value = arg.value(); + if (value.isEmpty()) { + value = "<" + arg.description()+ ">"; // NOI18N + } + values.add(value); + } + } + connector = new DebugConnector(NAME_ATTACH_BY + connectorName, type, + names, values); + } + } + connectors.add(connector); + } + connectors.sort((c1, c2) -> c1.getName().compareToIgnoreCase(c2.getName())); + return connectors; + } + + private static String getArgumentOrDefault(Connector.Argument arg, String def) { + if (arg != null) { + String value = arg.value(); + if (!value.isEmpty()) { + return value; + } + } + return def; + } + + public static CompletableFuture<Object> findProcessAttachTo(NbCodeLanguageClient client) { + return CompletableFuture.supplyAsync(() -> { + return listProcessesToAttachTo(client); + }, RP).thenCompose(params -> client.showQuickPick(params)).thenApply(itemsList -> { + if (itemsList == null || itemsList.isEmpty()) { + return null; + } else { + return itemsList.get(0).getUserData(); + } + }); + } + + @Messages("MSG_NoDebuggableProcess=No debuggable JVM process found. Please be sure to use `-agentlib:jdwp=transport=dt_socket,server=y` option.") + private static void notifyNoProcessesError(NbCodeLanguageClient client) { + MessageParams params = new MessageParams(); + params.setMessage(Bundle.MSG_NoDebuggableProcess()); + params.setType(MessageType.Error); + client.showMessage(params); + } + + @Messages("LBL_PickProcessAttach=Pick JVM process to attach to:") + private static ShowQuickPickParams listProcessesToAttachTo(NbCodeLanguageClient client) { + List<QuickPickItem> attachables = new ArrayList<>(); + List<VirtualMachineDescriptor> descriptors = VirtualMachine.list(); + for (VirtualMachineDescriptor descriptor : descriptors) { + try { + VirtualMachine vm = VirtualMachine.attach(descriptor); + Properties agentProperties = vm.getAgentProperties(); + boolean hasJDWP = false; + for (Object key : agentProperties.keySet()) { + if (key instanceof String && ((String) key).contains("jdwp")) { // NOI18N + hasJDWP = true; + break; + } + } + if (hasJDWP) { + attachables.add(createQuickPickItem(descriptor, vm)); + } + } catch (AttachNotSupportedException | IOException ex) { + continue; + } + } + if (attachables.isEmpty()) { + notifyNoProcessesError(client); + throw ErrorUtilities.createResponseErrorException("No debuggable JVM process found.", ResponseErrorCode.RequestCancelled); // NOI18N + } + return new ShowQuickPickParams(Bundle.LBL_PickProcessAttach(), attachables); + } + + private static QuickPickItem createQuickPickItem(VirtualMachineDescriptor descriptor, VirtualMachine vm) { + String label = descriptor.id(); + String detail = descriptor.displayName(); + if ("Unknown".equals(detail)) { // NOI18N + String command = null; + try { + command = vm.getSystemProperties().getProperty("sun.java.command"); // NOI18N + } catch (IOException ex) {} + if (command == null) { + try { + command = vm.getAgentProperties().getProperty("sun.java.command"); // NOI18N + } catch (IOException ex) {} + } + if (command != null) { + detail = command; + } + } + int index = detail.indexOf(' '); + String description = index > 0 ? detail.substring(0, index) : detail; + if (index <= 0) { + // Do not duplicate the description. + detail = null; + } + Object userData = descriptor.id(); // Process Id + return new QuickPickItem(label, description, detail, false, userData); + } + +} diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/NbAttachRequestHandler.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/NbAttachRequestHandler.java new file mode 100644 index 0000000..8b403f8 --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/attach/NbAttachRequestHandler.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.lsp.server.debugging.attach; + +import com.sun.jdi.Bootstrap; +import com.sun.jdi.VirtualMachineManager; +import com.sun.jdi.connect.AttachingConnector; +import com.sun.jdi.connect.Connector.Argument; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.MessageType; +import org.eclipse.lsp4j.debug.TerminatedEventArguments; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; + +import org.netbeans.api.debugger.DebuggerEngine; +import org.netbeans.api.debugger.DebuggerInfo; +import org.netbeans.api.debugger.DebuggerManager; +import org.netbeans.api.debugger.Session; +import org.netbeans.api.debugger.jpda.AttachingDICookie; +import org.netbeans.api.debugger.jpda.DebuggerStartException; +import org.netbeans.api.debugger.jpda.JPDADebugger; +import org.netbeans.modules.java.lsp.server.debugging.DebugAdapterContext; +import org.netbeans.modules.java.lsp.server.debugging.launch.NbDebugSession; +import org.netbeans.modules.java.lsp.server.debugging.utils.ErrorUtilities; +import org.netbeans.modules.java.lsp.server.protocol.NbCodeLanguageClient; +import org.openide.util.NbBundle.Messages; +import org.openide.util.RequestProcessor; + +/** + * + * @author Martin Entlicher + */ +public final class NbAttachRequestHandler { + + private static final String CONNECTOR_ARG_PID = "pid"; // NOI18N + private static final String CONNECTOR_ARG_HOST = "hostname"; // NOI18N + private static final String CONNECTOR_ARG_PORT = "port"; // NOI18N + private static final String CONNECTOR_ARG_NAME = "name"; // NOI18N + // The default attributes of DebugConfiguration + private static final Set<String> CONFIG_ATTRIBUTES = new HashSet<>(Arrays.asList("type", "name", "request", "classPaths", "console")); // NOI18N + + private static final RequestProcessor RP = new RequestProcessor(AttachConfigurations.class); + + public CompletableFuture<Void> attach(Map<String, Object> attachArguments, DebugAdapterContext context) { + boolean isNative = "nativeimage".equals(attachArguments.get("type")); // NOI18N + if (isNative) { + return attachToNative(attachArguments, context); + } else { + return attachToJVM(attachArguments, context); + } + } + + private CompletableFuture<Void> attachToNative(Map<String, Object> attachArguments, DebugAdapterContext context) { + CompletableFuture<Void> resultFuture = new CompletableFuture<>(); + // TODO + ErrorUtilities.completeExceptionally(resultFuture, + "Attach to native image is not implemented yet", ResponseErrorCode.serverErrorStart); + return resultFuture; + } + + @Messages({"# {0} - connector name", "MSG_InvalidConnector=Invalid connector name: {0}"}) + private CompletableFuture<Void> attachToJVM(Map<String, Object> attachArguments, DebugAdapterContext context) { + String name = (String) attachArguments.get("name"); // NOI18N + AttachingDICookie attachingCookie; + String connectorName; + Map<String, String> translatedArguments = new HashMap<>(); + CompletableFuture<Void> resultFuture = new CompletableFuture<>(); + switch (name) { + case AttachConfigurations.NAME_ATTACH_PROCESS: + Object pid = attachArguments.get(AttachConfigurations.PROCESS_ARG_PID); + connectorName = AttachConfigurations.CONNECTOR_PROCESS; + translatedArguments.put(AttachConfigurations.PROCESS_ARG_PID, CONNECTOR_ARG_PID); + break; + case AttachConfigurations.NAME_ATTACH_SOCKET: + connectorName = AttachConfigurations.CONNECTOR_SOCKET; + translatedArguments.put(AttachConfigurations.SOCKET_ARG_HOST, CONNECTOR_ARG_HOST); + translatedArguments.put(AttachConfigurations.SOCKET_ARG_PORT, CONNECTOR_ARG_PORT); + break; + case AttachConfigurations.NAME_ATTACH_SHMEM: + connectorName = AttachConfigurations.CONNECTOR_SHMEM; + translatedArguments.put(AttachConfigurations.SHMEM_ARG_NAME, CONNECTOR_ARG_NAME); + break; + default: + if (name.startsWith(AttachConfigurations.NAME_ATTACH_BY)) { + connectorName = name.substring(AttachConfigurations.NAME_ATTACH_BY.length()); + } else { + ErrorUtilities.completeExceptionally(resultFuture, + Bundle.MSG_InvalidConnector(name), + ResponseErrorCode.serverErrorStart); + connectorName = null; + } + } + if (connectorName != null) { + context.setDebugMode(true); + RP.post(() -> attachTo(connectorName, attachArguments, translatedArguments, context, resultFuture)); + } else { + assert resultFuture.isCompletedExceptionally(); + } + return resultFuture; + } + + @Messages({"# {0} - connector name", "# {1} - argument name", "MSG_ConnectorArgumentNotFound=Argument {0} of {1} was not found.", + "# {0} - argument name", "# {1} - value", "MSG_ConnectorInvalidValue=Invalid value of {0}: {1}", + "# {0} - connector name", "MSG_ConnectorNotFound=Connector {0} was not found."}) + private void attachTo(String connectorName, Map<String, Object> arguments, Map<String, String> translatedArguments, DebugAdapterContext context, CompletableFuture<Void> resultFuture) { + VirtualMachineManager vmm = Bootstrap.virtualMachineManager (); + List<AttachingConnector> attachingConnectors = vmm.attachingConnectors(); + for (AttachingConnector connector : attachingConnectors) { + if (connector.name().equals(connectorName)) { + Map<String, Argument> args = connector.defaultArguments(); + for (String argName : arguments.keySet()) { + if (CONFIG_ATTRIBUTES.contains(argName) || argName.startsWith("__")) { + continue; + } + String argNameTranslated = translatedArguments.getOrDefault(argName, argName); + Argument arg = args.get(argNameTranslated); + if (arg == null) { + ErrorUtilities.completeExceptionally(resultFuture, + Bundle.MSG_ConnectorArgumentNotFound(connectorName, argNameTranslated), + ResponseErrorCode.serverErrorStart); + return ; + } + String value = arguments.get(argName).toString(); + if (!arg.isValid(value)) { + ErrorUtilities.completeExceptionally(resultFuture, + Bundle.MSG_ConnectorInvalidValue(argName, value), + ResponseErrorCode.serverErrorStart); + return ; + } + arg.setValue(value); + } + AttachingDICookie attachingCookie = AttachingDICookie.create(connector, args); + resultFuture.complete(null); + startAttaching(attachingCookie, context); + return ; + } + } + ErrorUtilities.completeExceptionally(resultFuture, + Bundle.MSG_ConnectorNotFound(connectorName), + ResponseErrorCode.serverErrorStart); + } + + @Messages("MSG_FailedToAttach=Failed to attach.") + private void startAttaching(AttachingDICookie attachingCookie, DebugAdapterContext context) { + DebuggerEngine[] es = DebuggerManager.getDebuggerManager ().startDebugging( + DebuggerInfo.create(AttachingDICookie.ID, new Object [] { attachingCookie }) + ); + if (es.length > 0) { + JPDADebugger debugger = es[0].lookupFirst(null, JPDADebugger.class); + if (debugger != null) { + Session session = es[0].lookupFirst(null, Session.class); + NbDebugSession debugSession = new NbDebugSession(session); + context.setDebugSession(debugSession); + AtomicBoolean finished = new AtomicBoolean(false); + debugger.addPropertyChangeListener(JPDADebugger.PROP_STATE, new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + int newState = (int) evt.getNewValue(); + if (newState == JPDADebugger.STATE_DISCONNECTED) { + if (!finished.getAndSet(true)) { + notifyTerminated(context); + } + } + } + }); + boolean success = false; + try { + debugger.waitRunning(); + success = debugger.getState() != JPDADebugger.STATE_DISCONNECTED; + } catch (DebuggerStartException ex) { + notifyErrorMessage(context, ex.getLocalizedMessage()); + } + if (!success) { + if (!finished.getAndSet(true)) { + notifyTerminated(context); + } + } + return ; + } + } + notifyErrorMessage(context, Bundle.MSG_FailedToAttach()); + notifyTerminated(context); + } + + private void notifyErrorMessage(DebugAdapterContext context, String message) { + MessageParams params = new MessageParams(); + params.setMessage(message); + params.setType(MessageType.Error); + context.getLspSession().getLookup().lookup(NbCodeLanguageClient.class).showMessage(params); + } + + private void notifyTerminated(DebugAdapterContext context) { + context.getClient().terminated(new TerminatedEventArguments()); + } +} diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbDebugSession.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbDebugSession.java index e18052c..3c110a6 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbDebugSession.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbDebugSession.java @@ -31,7 +31,7 @@ public final class NbDebugSession { private final Session session; private volatile NIDebugger niDebugger; - NbDebugSession(Session session) { + public NbDebugSession(Session session) { this.session = session; } diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/DebugConnector.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/DebugConnector.java new file mode 100644 index 0000000..e8b3a20 --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/DebugConnector.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.lsp.server.protocol; + +import java.util.List; +import java.util.Objects; +import org.eclipse.lsp4j.jsonrpc.validation.NonNull; +import org.eclipse.xtext.xbase.lib.Pure; +import org.eclipse.xtext.xbase.lib.util.ToStringBuilder; + +/** + * Debug connector arguments. + * + * @author Martin Entlicher + */ +public final class DebugConnector { + + /** + * The display name identifier of the connector. + */ + @NonNull + private String name; + + /** + * The type of the connector. + */ + @NonNull + private String type; + + /** + * The debug connector arguments. + */ + @NonNull + private List<String> arguments; + + /** + * Default values of debug connector arguments. + */ + @NonNull + private List<String> defaultValues; + + public DebugConnector(String name, String type, List<String> arguments, List<String> defaultValues) { + this.name = name; + this.type = type; + this.arguments = arguments; + this.defaultValues = defaultValues; + } + + @Pure + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Pure + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Pure + public List<String> getArguments() { + return arguments; + } + + public void setArguments(List<String> arguments) { + this.arguments = arguments; + } + + @Pure + public List<String> getDefaultValues() { + return defaultValues; + } + + public void setDefaultValues(List<String> defaultValues) { + this.defaultValues = defaultValues; + } + + @Pure + @Override + public int hashCode() { + int hash = 7; + hash = 37 * hash + Objects.hashCode(this.name); + hash = 37 * hash + Objects.hashCode(this.type); + hash = 37 * hash + Objects.hashCode(this.arguments); + hash = 37 * hash + Objects.hashCode(this.defaultValues); + return hash; + } + + @Pure + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DebugConnector other = (DebugConnector) obj; + if (!Objects.equals(this.name, other.name)) { + return false; + } + if (!Objects.equals(this.type, other.type)) { + return false; + } + if (!Objects.equals(this.arguments, other.arguments)) { + return false; + } + if (!Objects.equals(this.defaultValues, other.defaultValues)) { + return false; + } + return true; + } + + @Pure + @Override + public String toString() { + ToStringBuilder b = new ToStringBuilder(this); + b.add("name", name); + b.add("type", type); + b.add("arguments", arguments.toString()); + b.add("defaultValues", defaultValues.toString()); + return b.toString(); + } + +} diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java index cb184b2..3a19dea 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java @@ -606,9 +606,18 @@ public final class Server { capabilities.setImplementationProvider(true); capabilities.setDocumentHighlightProvider(true); capabilities.setReferencesProvider(true); - List<String> commands = new ArrayList<>(Arrays.asList(JAVA_NEW_FROM_TEMPLATE, JAVA_BUILD_WORKSPACE, JAVA_GET_PROJECT_SOURCE_ROOTS, - JAVA_GET_PROJECT_CLASSPATH, JAVA_GET_PROJECT_PACKAGES,JAVA_LOAD_WORKSPACE_TESTS, GRAALVM_PAUSE_SCRIPT, JAVA_SUPER_IMPLEMENTATION, - JAVA_FIND_CONFIGURATIONS)); + List<String> commands = new ArrayList<>(Arrays.asList( + GRAALVM_PAUSE_SCRIPT, + JAVA_BUILD_WORKSPACE, + JAVA_FIND_DEBUG_ATTACH_CONFIGURATIONS, + JAVA_FIND_DEBUG_PROCESS_TO_ATTACH, + JAVA_FIND_PROJECT_CONFIGURATIONS, + JAVA_GET_PROJECT_CLASSPATH, + JAVA_GET_PROJECT_PACKAGES, + JAVA_GET_PROJECT_SOURCE_ROOTS, + JAVA_LOAD_WORKSPACE_TESTS, + JAVA_NEW_FROM_TEMPLATE, + JAVA_SUPER_IMPLEMENTATION)); for (CodeGenerator codeGenerator : Lookup.getDefault().lookupAll(CodeGenerator.class)) { commands.addAll(codeGenerator.getCommands()); } @@ -731,8 +740,16 @@ public final class Server { /** * Enumerates project configurations. */ - public static final String JAVA_FIND_CONFIGURATIONS = "java.project.configurations"; - + public static final String JAVA_FIND_PROJECT_CONFIGURATIONS = "java.project.configurations"; + /** + * Enumerates attach debugger configurations. + */ + public static final String JAVA_FIND_DEBUG_ATTACH_CONFIGURATIONS = "java.attachDebugger.configurations"; + /** + * Enumerates JVM processes eligible for debugger attach. + */ + public static final String JAVA_FIND_DEBUG_PROCESS_TO_ATTACH = "java.attachDebugger.pickProcess"; + static final String INDEXING_COMPLETED = "Indexing completed."; static final String NO_JAVA_SUPPORT = "Cannot initialize Java support on JDK "; diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java index 8fe666d..7f970ad 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java @@ -78,6 +78,7 @@ import org.netbeans.api.project.ui.OpenProjects; import org.netbeans.modules.gsf.testrunner.ui.api.TestMethodController; import org.netbeans.modules.java.lsp.server.LspServerState; import org.netbeans.modules.java.lsp.server.Utils; +import org.netbeans.modules.java.lsp.server.debugging.attach.AttachConfigurations; import org.netbeans.modules.java.source.ui.JavaSymbolProvider; import org.netbeans.modules.java.source.ui.JavaTypeProvider; import org.netbeans.modules.java.source.usages.ClassIndexImpl; @@ -219,7 +220,7 @@ public final class WorkspaceServiceImpl implements WorkspaceService, LanguageCli Position pos = gson.fromJson(gson.toJson(params.getArguments().get(1)), Position.class); return (CompletableFuture)((TextDocumentServiceImpl)server.getTextDocumentService()).superImplementations(uri, pos); - case Server.JAVA_FIND_CONFIGURATIONS: { + case Server.JAVA_FIND_PROJECT_CONFIGURATIONS: { String fileUri = ((JsonPrimitive) params.getArguments().get(0)).getAsString(); FileObject file; @@ -232,6 +233,12 @@ public final class WorkspaceServiceImpl implements WorkspaceService, LanguageCli return findProjectConfigurations(file); } + case Server.JAVA_FIND_DEBUG_ATTACH_CONFIGURATIONS: { + return AttachConfigurations.findConnectors(); + } + case Server.JAVA_FIND_DEBUG_PROCESS_TO_ATTACH: { + return AttachConfigurations.findProcessAttachTo(client); + } default: for (CodeGenerator codeGenerator : Lookup.getDefault().lookupAll(CodeGenerator.class)) { if (codeGenerator.getCommands().contains(command)) { @@ -249,8 +256,10 @@ public final class WorkspaceServiceImpl implements WorkspaceService, LanguageCli } ProjectConfigurationProvider<ProjectConfiguration> provider = p.getLookup().lookup(ProjectConfigurationProvider.class); List<String> configDispNames = new ArrayList<>(); - for (ProjectConfiguration c : provider.getConfigurations()) { - configDispNames.add(c.getDisplayName()); + if (provider != null) { + for (ProjectConfiguration c : provider.getConfigurations()) { + configDispNames.add(c.getDisplayName()); + } } return configDispNames; }); diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java index 075782b..51a4bcd 100644 --- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java +++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java @@ -1076,6 +1076,48 @@ public class ServerTest extends NbTestCase { assertEquals(13, loc.getRange().getEnd().getCharacter()); } + public void testFindDebugAttachConfigurations() throws Exception { + Launcher<LanguageServer> serverLauncher = LSPLauncher.createClientLauncher(new LspClient() { + }, client.getInputStream(), client.getOutputStream()); + serverLauncher.startListening(); + LanguageServer server = serverLauncher.getRemoteProxy(); + server.initialize(new InitializeParams()).get(); + Object ret = server.getWorkspaceService().executeCommand(new ExecuteCommandParams(Server.JAVA_FIND_DEBUG_ATTACH_CONFIGURATIONS, Collections.emptyList())).get(); + assertNotNull(ret); + DebugConnector[] connectors = gson.fromJson(gson.toJsonTree(ret).getAsJsonArray(), DebugConnector[].class); + assertTrue(connectors.length > 0); + boolean haveAttachToPort = false; + boolean haveAttachToProcess = false; + for (DebugConnector c : connectors) { + if ("Attach to Port".equals(c.getName())) { + haveAttachToPort = true; + checkAttachToPort(c); + } + if ("Attach to Process".equals(c.getName())) { + haveAttachToProcess = true; + checkAttachToProcess(c); + } + } + assertTrue(Arrays.toString(connectors), haveAttachToPort && haveAttachToProcess); + } + + private void checkAttachToPort(DebugConnector c) { + assertEquals("java8+", c.getType()); + List<String> arguments = c.getArguments(); + assertEquals(2, arguments.size()); + assertEquals("hostName", arguments.get(0)); + assertEquals("port", arguments.get(1)); + assertEquals(2, c.getDefaultValues().size()); + } + + private void checkAttachToProcess(DebugConnector c) { + assertEquals("java8+", c.getType()); + List<String> arguments = c.getArguments(); + assertEquals(1, arguments.size()); + assertEquals("processId", arguments.get(0)); + assertEquals(1, c.getDefaultValues().size()); + } + public void testOpenProjectOpenJDK() throws Exception { getWorkDir().mkdirs(); diff --git a/java/java.lsp.server/vscode/package.json b/java/java.lsp.server/vscode/package.json index 8e871d6..dbf3a5f 100644 --- a/java/java.lsp.server/vscode/package.json +++ b/java/java.lsp.server/vscode/package.json @@ -28,7 +28,8 @@ "workspaceContains:**/*.java", "workspaceContains:pom.xml", "workspaceContains:build.gradle", - "onDebug" + "onDebug", + "onDebugDynamicConfigurations" ], "main": "./out/extension.js", "contributes": { @@ -125,7 +126,7 @@ "description": "Arguments for the Java VM", "default": null }, - "launchConfiguration" : { + "launchConfiguration": { "type": [ "string", "null" @@ -133,6 +134,34 @@ "description": "Mode and default behaviour for launch" } } + }, + "attach": { + "properties": { + "hostName": { + "type": "string", + "default": "localhost", + "description": "Host name or IP address to which to attach" + }, + "port": { + "type": "string", + "default": "8000", + "description": "Port number to which to attach" + }, + "sharedMemoryName": { + "type": "string", + "description": "Shared memory name of the debuggee" + }, + "processId": { + "type": "string", + "default": "${command:java.attachDebugger.pickProcess}", + "description": "Process Id of the debuggee" + }, + "timeout": { + "type": "string", + "default": "30000", + "description": "Timeout while waiting to attach" + } + } } }, "initialConfigurations": [ @@ -297,16 +326,16 @@ "nbjavac": "node ./out/nbcode.js -J-Dnetbeans.close=true --modules --install .*nbjavac.*" }, "devDependencies": { - "@types/vscode": "^1.49.0", "@types/glob": "^7.1.1", "@types/mocha": "^7.0.2", "@types/node": "^13.11.0", "@types/ps-node": "^0.1.0", + "@types/vscode": "^1.49.0", "glob": "^7.1.6", "mocha": "^7.1.2", + "ps-node": "^0.1.6", "typescript": "^3.8.3", - "vscode-test": "^1.3.0", - "ps-node": "^0.1.6" + "vscode-test": "^1.3.0" }, "dependencies": { "vscode-debugadapter": "1.42.1", diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts index ada41c6..7027ff6 100644 --- a/java/java.lsp.server/vscode/src/extension.ts +++ b/java/java.lsp.server/vscode/src/extension.ts @@ -41,7 +41,7 @@ import { testExplorerExtensionId, TestHub } from 'vscode-test-adapter-api'; import { TestAdapterRegistrar } from 'vscode-test-adapter-util'; import * as launcher from './nbcode'; import {NbTestAdapter} from './testAdapter'; -import { StatusMessageRequest, ShowStatusMessageParams, QuickPickRequest, InputBoxRequest, TestProgressNotification } from './protocol'; +import { StatusMessageRequest, ShowStatusMessageParams, QuickPickRequest, InputBoxRequest, TestProgressNotification, DebugConnector } from './protocol'; const API_VERSION : string = "1.0"; let client: Promise<LanguageClient>; @@ -173,7 +173,7 @@ export function activate(context: ExtensionContext): VSNetBeansAPI { //register debugger: let configProvider = new NetBeansConfigurationProvider(); - context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('java8+', configProvider)); + context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('java8+', configProvider, vscode.DebugConfigurationProviderTriggerKind.Dynamic)); let configNativeProvider = new NetBeansConfigurationNativeProvider(); context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('nativeimage', configNativeProvider)); @@ -672,30 +672,43 @@ class NetBeansConfigurationProvider implements vscode.DebugConfigurationProvider } else { u = vscode.window.activeTextEditor?.document?.uri } + let result : vscode.DebugConfiguration[] = []; const configNames : string[] | null | undefined = await vscode.commands.executeCommand('java.project.configurations', u?.toString()); - if (!configNames) { - return []; + if (configNames) { + let first : boolean = true; + for (let cn of configNames) { + let cname : string; + + if (first) { + // ignore the default config, comes first. + first = false; + continue; + } else { + cname = "Launch Java: " + cn; + } + const debugConfig : vscode.DebugConfiguration = { + name: cname, + type: "java8+", + request: "launch", + mainClass: '${file}', + launchConfiguration: cn, + }; + result.push(debugConfig); + } } - let result : vscode.DebugConfiguration[] = []; - let first : boolean = true; - for (let cn of configNames) { - let cname : string; - - if (first) { - // ignore the default config, comes first. - first = false; - continue; - } else { - cname = "Launch Java: " + cn; + const attachConnectors : DebugConnector[] | null | undefined = await vscode.commands.executeCommand('java.attachDebugger.configurations'); + if (attachConnectors) { + for (let ac of attachConnectors) { + const debugConfig : vscode.DebugConfiguration = { + name: ac.name, + type: ac.type, + request: "attach", + }; + for (let i = 0; i < ac.arguments.length; i++) { + debugConfig[ac.arguments[i]] = ac.defaultValues[i]; + } + result.push(debugConfig) } - const debugConfig : vscode.DebugConfiguration = { - name: cname, - type: "java8+", - request: "launch", - mainClass: '${file}', - launchConfiguration: cn, - }; - result.push(debugConfig); } return result; } @@ -707,7 +720,7 @@ class NetBeansConfigurationProvider implements vscode.DebugConfigurationProvider if (!config.request) { config.request = 'launch'; } - if (!config.mainClass) { + if ('launch' == config.request && !config.mainClass) { config.mainClass = '${file}'; } if (!config.classPaths) { diff --git a/java/java.lsp.server/vscode/src/protocol.ts b/java/java.lsp.server/vscode/src/protocol.ts index 8a4d08f..5b48c2e 100644 --- a/java/java.lsp.server/vscode/src/protocol.ts +++ b/java/java.lsp.server/vscode/src/protocol.ts @@ -96,3 +96,10 @@ export interface TestCase { export namespace TestProgressNotification { export const type = new NotificationType<TestProgressParams, void>('window/notifyTestProgress'); }; + +export interface DebugConnector { + name: string; + type: string; + arguments: string[]; + defaultValues: string[]; +} --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@netbeans.apache.org For additional commands, e-mail: commits-h...@netbeans.apache.org For further information about the NetBeans mailing lists, visit: https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists