From b6e883719e2a3bc66296bde952dad18b105bac27 Mon Sep 17 00:00:00 2001 From: Mickael Istria Date: Fri, 7 Apr 2023 10:58:33 +0200 Subject: [PATCH] Use vscode-js-debug instead of deprecated node-debug2 This also restore ability to debug running Node processes (eg start with --inspect). Requires usage of latest LSP4E support for DAP. Fixes https://github.com/eclipse-wildwebdeveloper/wildwebdeveloper/issues/555 --- .../wildwebdeveloper/tests/TestDebug.java | 98 +++- .../wildwebdeveloper/tests/TestDebugTS.java | 116 ----- .../testProjects/HelloWorldTS/.build/index.js | 4 +- .../HelloWorldTS/.build/index.js.map | 2 +- .../testProjects/HelloWorldTS/index.ts | 4 +- .../testProjects/HelloWorldTS/tsconfig.json | 5 +- org.eclipse.wildwebdeveloper/.gitignore | 1 + .../META-INF/MANIFEST.MF | 2 +- org.eclipse.wildwebdeveloper/build.properties | 1 + org.eclipse.wildwebdeveloper/package.json | 1 - org.eclipse.wildwebdeveloper/pom.xml | 39 +- .../debug/node/NodeAttachDebugDelegate.java | 76 +++- .../debug/node/NodeRunDAPDebugDelegate.java | 381 +--------------- .../debug/node/VSCodeJSDebugDelegate.java | 430 ++++++++++++++++++ target-platform/target-platform.target | 4 +- 15 files changed, 619 insertions(+), 545 deletions(-) delete mode 100644 org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestDebugTS.java create mode 100644 org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/VSCodeJSDebugDelegate.java diff --git a/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestDebug.java b/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestDebug.java index e652bdbdbb..a0677267d8 100644 --- a/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestDebug.java +++ b/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestDebug.java @@ -19,6 +19,7 @@ import java.io.File; import java.nio.file.Files; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -76,9 +77,17 @@ public void setUpLaunch() throws DebugException { private void removeAllLaunches() throws DebugException { for (ILaunch launch : this.launchManager.getLaunches()) { - launch.terminate(); + try { + launch.terminate(); + } catch (DebugException e) { + e.printStackTrace(); + } for (IDebugTarget debugTarget : launch.getDebugTargets()) { - debugTarget.terminate(); + try { + debugTarget.terminate(); + } catch (DebugException e) { + e.printStackTrace(); + } launch.removeDebugTarget(debugTarget); } for (IProcess process : launch.getProcesses()) { @@ -170,21 +179,18 @@ public boolean condition() { return launchManager.getDebugTargets().length > before.size(); } }.waitForCondition(Display.getDefault(), 30000), "New Debug Target not created"); - Set after = new HashSet<>(Arrays.asList(launchManager.getDebugTargets())); - after.removeAll(before); - assertEquals(1, after.size(), "Extra DebugTarget not found"); - IDebugTarget target = after.iterator().next(); assertTrue(new DisplayHelper() { @Override public boolean condition() { try { - return target.getThreads().length > 0; + return debugTargetWithThreads(before) != null; } catch (DebugException e) { e.printStackTrace(); return false; } } }.waitForCondition(Display.getDefault(), 30000), "Debug Target shows no threads"); + IDebugTarget target = debugTargetWithThreads(before); assertTrue(new DisplayHelper() { @Override public boolean condition() { @@ -221,4 +227,82 @@ protected boolean condition() { }).findAny().get(); assertEquals("1605", nVariable.getValue().getValueString()); } + + private IDebugTarget debugTargetWithThreads(Collection toExclude) throws DebugException { + Set current = new HashSet<>(Arrays.asList(launchManager.getDebugTargets())); + current.removeAll(toExclude); + for (IDebugTarget target : current) { + if (target.getThreads().length > 0) { + return target; + } + } + return null; + } + + @Test + public void testFindThreadsAndHitsBreakpointTypeScript() throws Exception { + IProject project = Utils.provisionTestProject("HelloWorldTS"); + IFile tsFile = project.getFile("index.ts"); + ITextEditor editor = (ITextEditor) IDE + .openEditor(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), tsFile); + IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + TextSelection selection = new TextSelection(doc, doc.getLineOffset(2) + 1, 0); + IToggleBreakpointsTarget toggleBreakpointsTarget = DebugUITools.getToggleBreakpointsTargetManager() + .getToggleBreakpointsTarget(editor, selection); + toggleBreakpointsTarget.toggleLineBreakpoints(editor, selection); + Set before = new HashSet<>(Arrays.asList(launchManager.getDebugTargets())); + DisplayHelper.sleep(1000); + new NodeRunDebugLaunchShortcut().launch(editor, ILaunchManager.DEBUG_MODE); + assertTrue(new DisplayHelper() { + @Override + public boolean condition() { + try { + return debugTargetWithThreads(before) != null; + } catch (DebugException e) { + e.printStackTrace(); + return false; + } + } + }.waitForCondition(Display.getDefault(), 30000), "Debug Target shows no threads"); + IDebugTarget target = debugTargetWithThreads(before); + assertTrue(new DisplayHelper() { + @Override + public boolean condition() { + try { + return Arrays.stream(target.getThreads()).anyMatch(ISuspendResume::isSuspended); + } catch (DebugException e) { + e.printStackTrace(); + return false; + } + } + }.waitForCondition(Display.getDefault(), 3000), "No thread is suspended"); + IThread suspendedThread = Arrays.stream(target.getThreads()).filter(ISuspendResume::isSuspended).findFirst() + .get(); + assertTrue(new DisplayHelper() { + @Override + protected boolean condition() { + try { + return suspendedThread.getStackFrames().length > 0 + && suspendedThread.getStackFrames()[0].getVariables().length > 0; + } catch (Exception ex) { + // ignore + return false; + } + } + }.waitForCondition(Display.getDefault(), 3000), "Suspended Thread doesn't show variables"); + IVariable closureVar = null; + for (IVariable variable : suspendedThread.getStackFrames()[0].getVariables()) { + if ("Closure".equals(variable.getName())) { + closureVar = variable; + } + } + IVariable userVariable = null; + for (IVariable variable : closureVar.getValue().getVariables()) { + if ("user".equals(variable.getName())) { + userVariable = variable; + } + } + assertEquals("'Eclipse User'", userVariable.getValue().getValueString()); + } + } diff --git a/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestDebugTS.java b/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestDebugTS.java deleted file mode 100644 index 9908357fc0..0000000000 --- a/org.eclipse.wildwebdeveloper.tests/src/org/eclipse/wildwebdeveloper/tests/TestDebugTS.java +++ /dev/null @@ -1,116 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2020 Red Hat Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Victor Rubezhny (Red Hat Inc.) - Initial implementation - *******************************************************************************/ -package org.eclipse.wildwebdeveloper.tests; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IProject; -import org.eclipse.debug.core.DebugException; -import org.eclipse.debug.core.ILaunchManager; -import org.eclipse.debug.core.model.IDebugTarget; -import org.eclipse.debug.core.model.ISuspendResume; -import org.eclipse.debug.core.model.IThread; -import org.eclipse.debug.core.model.IVariable; -import org.eclipse.debug.ui.DebugUITools; -import org.eclipse.debug.ui.actions.IToggleBreakpointsTarget; -import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.TextSelection; -import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.ide.IDE; -import org.eclipse.ui.tests.harness.util.DisplayHelper; -import org.eclipse.ui.texteditor.ITextEditor; -import org.eclipse.wildwebdeveloper.debug.node.NodeRunDebugLaunchShortcut; -import org.junit.jupiter.api.Test; - -@SuppressWarnings("restriction") -public class TestDebugTS extends TestDebug { - @Override - @Test - public void testFindThreadsAndHitsBreakpoint() throws Exception { - IProject project = Utils.provisionTestProject("HelloWorldTS"); - IFile tsFile = project.getFile("index.ts"); - ITextEditor editor = (ITextEditor) IDE - .openEditor(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), tsFile); - IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput()); - TextSelection selection = new TextSelection(doc, doc.getLineOffset(1) + 1, 0); - IToggleBreakpointsTarget toggleBreakpointsTarget = DebugUITools.getToggleBreakpointsTargetManager() - .getToggleBreakpointsTarget(editor, selection); - toggleBreakpointsTarget.toggleLineBreakpoints(editor, selection); - Set before = new HashSet<>(Arrays.asList(launchManager.getDebugTargets())); - DisplayHelper.sleep(1000); - new NodeRunDebugLaunchShortcut().launch(editor, ILaunchManager.DEBUG_MODE); - assertTrue(new DisplayHelper() { - @Override - public boolean condition() { - return launchManager.getDebugTargets().length > before.size(); - } - }.waitForCondition(Display.getDefault(), 30000), "New Debug Target not created"); - Set after = new HashSet<>(Arrays.asList(launchManager.getDebugTargets())); - after.removeAll(before); - assertEquals(1, after.size(), "Extra DebugTarget not found"); - IDebugTarget target = after.iterator().next(); - assertTrue(new DisplayHelper() { - @Override - public boolean condition() { - try { - return target.getThreads().length > 0; - } catch (DebugException e) { - e.printStackTrace(); - return false; - } - } - }.waitForCondition(Display.getDefault(), 30000), "Debug Target shows no threads"); - assertTrue(new DisplayHelper() { - @Override - public boolean condition() { - try { - return Arrays.stream(target.getThreads()).anyMatch(ISuspendResume::isSuspended); - } catch (DebugException e) { - e.printStackTrace(); - return false; - } - } - }.waitForCondition(Display.getDefault(), 3000), "No thread is suspended"); - IThread suspendedThread = Arrays.stream(target.getThreads()).filter(ISuspendResume::isSuspended).findFirst() - .get(); - assertTrue(new DisplayHelper() { - @Override - protected boolean condition() { - try { - return suspendedThread.getStackFrames().length > 0 - && suspendedThread.getStackFrames()[0].getVariables().length > 0; - } catch (Exception ex) { - // ignore - return false; - } - } - }.waitForCondition(Display.getDefault(), 3000), "Suspended Thread doesn't show variables"); - IVariable localVariable = suspendedThread.getStackFrames()[0].getVariables()[0]; - assertEquals("Local", localVariable.getName()); - IVariable nVariable = Arrays.stream(localVariable.getValue().getVariables()).filter(var -> { - try { - return "user".equals(var.getName()); - } catch (DebugException e) { - return false; - } - }).findAny().get(); - assertEquals("\"Eclipse User\"", nVariable.getValue().getValueString()); - } -} diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/.build/index.js b/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/.build/index.js index 4229da68c1..488e259fc9 100644 --- a/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/.build/index.js +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/.build/index.js @@ -1,4 +1,4 @@ "use strict"; -var user = "Eclipse User"; -console.log("Hello world,, " + user + '!'); +let user = "Eclipse User"; +new Promise(resolve => setTimeout(resolve, 1000)).then(() => console.log("Hello world,, " + user + '!')); //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/.build/index.js.map b/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/.build/index.js.map index 3dd1d964e2..8816d80eb8 100644 --- a/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/.build/index.js.map +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/.build/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";AAAA,IAAI,IAAI,GAAU,cAAc,CAAC;AACjC,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,IAAI,GAAI,GAAG,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";AAAA,IAAI,IAAI,GAAU,cAAc,CAAC;AACjC,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CACxD,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,IAAI,GAAI,GAAG,CAAC,CAC9C,CAAC"} \ No newline at end of file diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/index.ts b/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/index.ts index ff8099c63f..bb9f706d8e 100644 --- a/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/index.ts +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/index.ts @@ -1,2 +1,4 @@ let user:string = "Eclipse User"; -console.log("Hello world,, " + user + '!'); \ No newline at end of file +new Promise(resolve => setTimeout(resolve, 1000)).then(() => + console.log("Hello world,, " + user + '!') +); \ No newline at end of file diff --git a/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/tsconfig.json b/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/tsconfig.json index 771f41ac1b..933496c599 100644 --- a/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/tsconfig.json +++ b/org.eclipse.wildwebdeveloper.tests/testProjects/HelloWorldTS/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2017", "module": "commonjs", "lib": ["es6", "dom"], "allowJs": true, @@ -8,7 +8,6 @@ "outDir": ".build/", "strict": true, "noImplicitAny": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "skipLibCheck": true } } diff --git a/org.eclipse.wildwebdeveloper/.gitignore b/org.eclipse.wildwebdeveloper/.gitignore index 61dda4e492..bf4242c708 100644 --- a/org.eclipse.wildwebdeveloper/.gitignore +++ b/org.eclipse.wildwebdeveloper/.gitignore @@ -2,4 +2,5 @@ target/ bin/ language-servers/ node_modules/ +js-debug/ package-lock.json \ No newline at end of file diff --git a/org.eclipse.wildwebdeveloper/META-INF/MANIFEST.MF b/org.eclipse.wildwebdeveloper/META-INF/MANIFEST.MF index 3f9aa4cd77..0aaadc6504 100644 --- a/org.eclipse.wildwebdeveloper/META-INF/MANIFEST.MF +++ b/org.eclipse.wildwebdeveloper/META-INF/MANIFEST.MF @@ -21,7 +21,7 @@ Require-Bundle: org.eclipse.ui, org.eclipse.core.resources;bundle-version="3.12.0", org.eclipse.debug.core;bundle-version="3.11.0", org.eclipse.debug.ui;bundle-version="3.11.0", - org.eclipse.lsp4e.debug;bundle-version="0.9.0", + org.eclipse.lsp4e.debug;bundle-version="0.15.5", com.google.gson;bundle-version="2.8.2", org.eclipse.core.expressions;bundle-version="3.6.0", org.eclipse.tm4e.languageconfiguration, diff --git a/org.eclipse.wildwebdeveloper/build.properties b/org.eclipse.wildwebdeveloper/build.properties index 4746789f00..1f9421c7bd 100644 --- a/org.eclipse.wildwebdeveloper/build.properties +++ b/org.eclipse.wildwebdeveloper/build.properties @@ -10,6 +10,7 @@ bin.includes = META-INF/,\ snippets/,\ icons/,\ schema/,\ + js-debug/,\ plugin.properties # See https://github.com/redhat-developer/yaml-language-server/issues/253 bin.excludes = node_modules/yaml-language-server/out/server/node_modules/ diff --git a/org.eclipse.wildwebdeveloper/package.json b/org.eclipse.wildwebdeveloper/package.json index b94c24a3e7..e544e9d4f9 100644 --- a/org.eclipse.wildwebdeveloper/package.json +++ b/org.eclipse.wildwebdeveloper/package.json @@ -15,7 +15,6 @@ "vscode-json-languageserver": "file:target/vscode-json-languageserver-1.3.4.tgz", "debugger-for-chrome": "file:target/debugger-for-chrome-4.13.0.tgz", "eslint-server": "file:target/eslint-server-2.4.1.tgz", - "node-debug2": "file:target/node-debug2-1.43.0.tgz", "@vue/language-server" : "1.8.15", "fsevents" : "2.3.3" } diff --git a/org.eclipse.wildwebdeveloper/pom.xml b/org.eclipse.wildwebdeveloper/pom.xml index 2bcd265ff0..ecda7d2559 100644 --- a/org.eclipse.wildwebdeveloper/pom.xml +++ b/org.eclipse.wildwebdeveloper/pom.xml @@ -66,42 +66,42 @@ - fetch-node-debug2 + fetch-chrome-debug-adapter generate-resources wget - https://ms-vscode.gallery.vsassets.io/_apis/public/gallery/publisher/ms-vscode/extension/node-debug2/1.43.0/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage - node-debug2.zip + https://msjsdiag.gallery.vsassets.io/_apis/public/gallery/publisher/msjsdiag/extension/debugger-for-chrome/4.13.0/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage + chromeDebugAdapter.zip true - ${project.build.directory}/node-debug2 + ${project.build.directory}/chrome-debug-adapter - fetch-chrome-debug-adapter + fetch-eslint-ls generate-resources wget - https://msjsdiag.gallery.vsassets.io/_apis/public/gallery/publisher/msjsdiag/extension/debugger-for-chrome/4.13.0/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage - chromeDebugAdapter.zip + https://dbaeumer.gallery.vsassets.io/_apis/public/gallery/publisher/dbaeumer/extension/vscode-eslint/2.4.2/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage + vscode-eslint-ls.zip true - ${project.build.directory}/chrome-debug-adapter + ${project.build.directory}/vscode-eslint-ls - fetch-eslint-ls + fetch-js-debug generate-resources wget - https://dbaeumer.gallery.vsassets.io/_apis/public/gallery/publisher/dbaeumer/extension/vscode-eslint/2.4.2/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage - vscode-eslint-ls.zip + https://github.com/microsoft/vscode-js-debug/releases/download/v1.83.0/js-debug-dap-v1.83.0.tar.gz + js-debug.tar.gz true - ${project.build.directory}/vscode-eslint-ls + ${project.basedir} @@ -148,19 +148,6 @@ - - prepare-node-debug2 - process-resources - - run - - - - - - - remove package-lock before install compile @@ -197,7 +184,6 @@ vscode/VSCode-linux-x64/resources/app/extensions/html-language-features/server vscode/VSCode-linux-x64/resources/app/extensions/css-language-features/server vscode/VSCode-linux-x64/resources/app/extensions/json-language-features/server - node-debug2/extension/ chrome-debug-adapter/extension/ vscode-eslint-ls/extension/server @@ -235,6 +221,7 @@ node_modules package-lock.json + js-debug diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/NodeAttachDebugDelegate.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/NodeAttachDebugDelegate.java index 934b553bb6..a568d4f9b6 100644 --- a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/NodeAttachDebugDelegate.java +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/NodeAttachDebugDelegate.java @@ -15,11 +15,16 @@ import java.io.File; import java.io.IOException; +import java.net.ServerSocket; import java.net.URL; -import java.util.Collections; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.FileLocator; @@ -27,17 +32,22 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.variables.VariablesPlugin; +import org.eclipse.debug.core.DebugEvent; +import org.eclipse.debug.core.DebugException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.IDebugEventSetListener; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.core.model.IProcess; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.lsp4e.debug.DSPPlugin; -import org.eclipse.lsp4e.debug.launcher.DSPLaunchDelegate; import org.eclipse.swt.widgets.Display; import org.eclipse.wildwebdeveloper.Activator; import org.eclipse.wildwebdeveloper.debug.LaunchConstants; import org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager; -public class NodeAttachDebugDelegate extends DSPLaunchDelegate { +public class NodeAttachDebugDelegate extends VSCodeJSDebugDelegate { static final String ID = "org.eclipse.wildwebdeveloper.launchConfiguration.nodeDebugAttach"; //$NON-NLS-1$ @@ -46,32 +56,82 @@ public class NodeAttachDebugDelegate extends DSPLaunchDelegate { static final String LOCAL_ROOT = "localRoot"; //$NON-NLS-1$ static final String REMOTE_ROOT = "remoteRoot"; //$NON-NLS-1$ + public NodeAttachDebugDelegate() { + super("pwa-node"); + } + @Override public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException { // user settings Map param = new HashMap<>(); + param.put("request", "attach"); param.put(ADDRESS, configuration.getAttribute(ADDRESS, "no address defined")); //$NON-NLS-1$ param.put(LaunchConstants.PORT, configuration.getAttribute(LaunchConstants.PORT, -1)); + param.put("type", type); + param.put("continueOnAttach", true); if (configuration.hasAttribute(LOCAL_ROOT)) { param.put(LOCAL_ROOT, VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(configuration.getAttribute(LOCAL_ROOT, ""))); } if (configuration.hasAttribute(REMOTE_ROOT)) { param.put(REMOTE_ROOT, configuration.getAttribute(REMOTE_ROOT, "")); } + File runtimeExecutable = NodeJSManager.getNodeJsLocation(); + if (runtimeExecutable != null) { + param.put(RUNTIME_EXECUTABLE, runtimeExecutable.getAbsolutePath()); + } try { - URL fileURL = FileLocator.toFileURL( - getClass().getResource("/node_modules/node-debug2/out/src/nodeDebug.js")); + URL fileURL = FileLocator.toFileURL(getClass().getResource(NODE_DEBUG_CMD)); File file = new File(fileURL.getPath()); - List debugCmdArgs = Collections.singletonList(file.getAbsolutePath()); + int port = 0; + try (ServerSocket serverSocket = new ServerSocket(0)) { + port = serverSocket.getLocalPort(); + serverSocket.close(); + } catch (IOException ex) { + Activator.getDefault().getLog().log(Status.error(ex.getMessage(), ex)); + } + Process vscodeJsDebugExec = DebugPlugin.exec(new String[] { runtimeExecutable.getAbsolutePath(), file.getAbsolutePath(), Integer.toString(port) }, new File(System.getProperty("user.dir")), new String[] { "DA_TEST_DISABLE_TELEMETRY=true"}, false); + IProcess vscodeJsDebugIProcess = DebugPlugin.newProcess(launch, vscodeJsDebugExec, "debug adapter"); + AtomicBoolean started = new AtomicBoolean(); + vscodeJsDebugIProcess.getStreamsProxy().getOutputStreamMonitor().addListener((text, mon) -> { + if (text.toLowerCase().contains("listening")) { + started.set(true); + } + }); + Instant request = Instant.now(); + while (!started.get() && Duration.between(request, Instant.now()).compareTo(Duration.ofSeconds(3)) < 3) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } DSPLaunchDelegateLaunchBuilder builder = new DSPLaunchDelegateLaunchBuilder(configuration, mode, launch, monitor); - builder.setLaunchDebugAdapter(NodeJSManager.getNodeJsLocation().getAbsolutePath(), debugCmdArgs); + builder.setAttachDebugAdapter("::1", port); builder.setMonitorDebugAdapter(configuration.getAttribute(DSPPlugin.ATTR_DSP_MONITOR_DEBUG_ADAPTER, false)); builder.setDspParameters(param); - super.launch(builder); + IDebugEventSetListener shutdownParentOnCompletion = new IDebugEventSetListener() { + @Override + public void handleDebugEvents(DebugEvent[] events) { + if (Arrays.stream(events).anyMatch(event -> + event.getKind() == DebugEvent.TERMINATE && + event.getSource() instanceof final IDebugTarget target && + target.getLaunch() == launch)) { + if (Arrays.stream(launch.getDebugTargets()).allMatch(IDebugTarget::isTerminated) + && List.of(vscodeJsDebugIProcess).equals(Arrays.stream(launch.getProcesses()).filter(Predicate.not(IProcess::isTerminated)).toList())) { + try { + vscodeJsDebugIProcess.terminate(); + } catch (DebugException ex) { + vscodeJsDebugExec.destroy(); + } + DebugPlugin.getDefault().removeDebugEventListener(this); + } + } + } + }; + DebugPlugin.getDefault().addDebugEventListener(shutdownParentOnCompletion); } catch (IOException e) { IStatus errorStatus = new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e); Activator.getDefault().getLog().log(errorStatus); diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/NodeRunDAPDebugDelegate.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/NodeRunDAPDebugDelegate.java index 6359b66359..f90a15a794 100644 --- a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/NodeRunDAPDebugDelegate.java +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/NodeRunDAPDebugDelegate.java @@ -13,386 +13,11 @@ *******************************************************************************/ package org.eclipse.wildwebdeveloper.debug.node; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.lang.reflect.Type; -import java.net.URL; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.regex.Pattern; - -import org.eclipse.core.resources.IContainer; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IFolder; -import org.eclipse.core.resources.IResource; -import org.eclipse.core.resources.IWorkspace; -import org.eclipse.core.resources.IWorkspaceRoot; -import org.eclipse.core.resources.IWorkspaceRunnable; -import org.eclipse.core.resources.ResourcesPlugin; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.FileLocator; -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.variables.VariablesPlugin; -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.internal.ui.DebugUIPlugin; -import org.eclipse.jface.dialogs.ErrorDialog; -import org.eclipse.jface.dialogs.MessageDialog; -import org.eclipse.lsp4e.debug.DSPPlugin; -import org.eclipse.lsp4e.debug.launcher.DSPLaunchDelegate; -import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.PartInitException; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.ide.IDE; -import org.eclipse.ui.part.FileEditorInput; -import org.eclipse.wildwebdeveloper.Activator; -import org.eclipse.wildwebdeveloper.debug.LaunchConstants; -import org.eclipse.wildwebdeveloper.debug.Messages; -import org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; - -@SuppressWarnings("restriction") -public class NodeRunDAPDebugDelegate extends DSPLaunchDelegate { +public class NodeRunDAPDebugDelegate extends VSCodeJSDebugDelegate { public static final String ID = "org.eclipse.wildwebdeveloper.launchConfiguration.nodeDebug"; //$NON-NLS-1$ - // see https://github.com/Microsoft/vscode-node-debug/blob/master/src/node/nodeDebug.ts LaunchRequestArguments - public static final String ARGUMENTS = "args"; //$NON-NLS-1$ - private static final String CWD = "cwd"; //$NON-NLS-1$ - private static final String ENV = "env"; //$NON-NLS-1$ - private static final String RUNTIME_EXECUTABLE = "runtimeExecutable"; //$NON-NLS-1$ - - public static final String NODE_DEBUG_CMD = "/node_modules/node-debug2/out/src/nodeDebug.js"; //$NON-NLS-1$ - public static final String TYPESCRIPT_CONTENT_TYPE = "org.eclipse.wildwebdeveloper.ts"; //$NON-NLS-1$ - public static final String JAVACRIPT_CONTENT_TYPE = "org.eclipse.wildwebdeveloper.js"; //$NON-NLS-1$ - - public static final String JAVACRIPT_DEBUGGABLE_PATTERNS = "__debuggablePatterns"; - public static final String JAVACRIPT_DEBUGGABLE_PATTERNS_DEFAULT = "[\"*.js\",\"*.es6\",\"*.jsx\",\"*.mjs\".\"*.cjs\"]"; - - - private static final String TS_CONFIG_NAME = "tsconfig.json"; //$NON-NLS-1$ - private static final String COMPILER_OPTIONS = "compilerOptions"; //$NON-NLS-1$ - private static final String SOURCE_MAP = "sourceMap"; //$NON-NLS-1$ - private static final String SOURCE_MAPS = "sourceMaps"; //$NON-NLS-1$ - private static final String MODULE = "module"; //$NON-NLS-1$ - private static final String MODULE_AMD = "amd"; //$NON-NLS-1$ - private static final String MODULE_SYSTEM = "system"; //$NON-NLS-1$ - private static final String OUT_DIR = "outDir"; //$NON-NLS-1$ - private static final String OUT_FILE = "outFile"; //$NON-NLS-1$ - private static final String ROOT_DIR = "rootDir"; //$NON-NLS-1$ - - @Override - public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) - throws CoreException { - // user settings - Map param = new HashMap<>(); - param.put(LaunchConstants.PROGRAM, VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(configuration.getAttribute(LaunchConstants.PROGRAM, "no program path defined"))); //$NON-NLS-1$ - String argsString = configuration.getAttribute(ARGUMENTS, "").trim(); //$NON-NLS-1$ - if (!argsString.isEmpty()) { - Object[] args = Arrays.asList(argsString.split(" ")).stream() //$NON-NLS-1$ - .filter(s -> !s.trim().isEmpty()) - .map(s -> { - try { - return VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(s); - } catch (CoreException e) { - IStatus errorStatus = new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e); - Activator.getDefault().getLog().log(errorStatus); - return s; - } - }) - .toArray(); - if (args.length > 0) { - param.put(ARGUMENTS, args); - } - } - Map env = configuration.getAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, - Collections.emptyMap()); - if (!env.isEmpty()) { - JsonObject envJson = new JsonObject(); - for (Entry entry : env.entrySet()) { - envJson.addProperty(entry.getKey(), VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(entry.getValue())); - } - param.put(ENV, envJson); - } - String cwd = configuration.getAttribute(DebugPlugin.ATTR_WORKING_DIRECTORY, "").trim(); //$NON-NLS-1$ - if (!cwd.isEmpty()) { - param.put(CWD, VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(cwd)); - } - File runtimeExecutable = NodeJSManager.getNodeJsLocation(); - if (runtimeExecutable != null) { - param.put(RUNTIME_EXECUTABLE, runtimeExecutable.getAbsolutePath()); - } - - if (!configureAdditionalParameters(param)) { - return; - } - - try { - URL fileURL = FileLocator.toFileURL( - getClass().getResource(NODE_DEBUG_CMD)); - File file = new File(fileURL.getPath()); - List debugCmdArgs = Collections.singletonList(file.getAbsolutePath()); - - DSPLaunchDelegateLaunchBuilder builder = new DSPLaunchDelegateLaunchBuilder(configuration, mode, launch, - monitor); - builder.setLaunchDebugAdapter(NodeJSManager.getNodeJsLocation().getAbsolutePath(), debugCmdArgs); - builder.setMonitorDebugAdapter(configuration.getAttribute(DSPPlugin.ATTR_DSP_MONITOR_DEBUG_ADAPTER, false)); - builder.setDspParameters(param); - super.launch(builder); - } catch (IOException e) { - IStatus errorStatus = new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e); - Activator.getDefault().getLog().log(errorStatus); - Display.getDefault().asyncExec(() -> ErrorDialog.openError(Display.getDefault().getActiveShell(), "Debug error", e.getMessage(), errorStatus)); //$NON-NLS-1$ - } - } - - private boolean configureAdditionalParameters(Map param) { - String program = (String)param.get(LaunchConstants.PROGRAM); - String cwd = (String)param.get(CWD); - - if (program == null) { - return false; - } - - File programFile = new File(program); - if (Platform.getContentTypeManager().getContentType(TYPESCRIPT_CONTENT_TYPE) - .isAssociatedWith(programFile.getName())) { - // TypeScript Source Mappings Configuration - File parentDirectory = cwd == null ? programFile.getParentFile() : new File(cwd); - File tsConfigFile = findTSConfigFile(parentDirectory); - if (tsConfigFile != null && tsConfigFile.exists()) { - parentDirectory = tsConfigFile.getParentFile(); - } - - String errorMessage = null; - Map tsConfig = readJSonFile(tsConfigFile); - - @SuppressWarnings("unchecked") - Map co = tsConfig == null ? null : (Map)tsConfig.get(COMPILER_OPTIONS); - if (co == null) { - errorMessage = Messages.NodeDebug_TSConfirError_NoTsConfig; - co = new HashMap<>(); - } - - //TS Compiler Options - param.putAll(co); - - if (errorMessage == null) { - Object option = co.get(SOURCE_MAP); - boolean sourceMap = option instanceof Boolean b && b.booleanValue(); - if (!sourceMap) { - errorMessage = Messages.NodeDebug_TSConfirError_SourceMapIsNotEnabled; - } - } - - // Override "outDir" option by converting it to an absolute path - boolean outDirOrFileIsSet = false; - Object option = co.get(MODULE); - String module = option instanceof String o ? o.trim() : null; - - option = co.get(OUT_DIR); - String outDir = option instanceof String o ? o.trim() : null; - if (outDir != null && outDir.length() > 0 && !".".equals(outDir) && !"./".equals(outDir)) { - File outDirFile = new File(parentDirectory, outDir); - try { - outDir = outDirFile.getCanonicalPath(); - } catch (IOException e) { - // Default to an absolute file path (non-checked) - outDir = outDirFile.getAbsolutePath(); - } - param.put(OUT_DIR, outDir); - outDirOrFileIsSet = true; - } - - option = co.get(OUT_FILE); - String outFile = option instanceof String o ? o.trim() : null; - if (outFile != null && outFile.length() != 0) { - File outFileFile = new File(parentDirectory, outFile); - try { - outFile = outFileFile.getCanonicalPath(); - } catch (IOException e) { - // Default to an absolute file path (non-checked) - outFile = outFileFile.getAbsolutePath(); - } - param.put(OUT_FILE, outFile); - outDirOrFileIsSet = true; - - if (!MODULE_AMD.equalsIgnoreCase(module) && !MODULE_SYSTEM.equalsIgnoreCase(module)) { - errorMessage = Messages.NodeDebug_TSConfigError_OutDirNotSupportedModule; - } - } - - option = co.get(ROOT_DIR); - String rootDir = option instanceof String o ? o.trim() : null; - if (rootDir != null && rootDir.length() > 0 && !".".equals(outDir) && !"./".equals(outDir)) { - File rootDirFile = new File(parentDirectory, rootDir); - try { - rootDir = rootDirFile.getCanonicalPath(); - } catch (IOException e) { - // Default to an absolute file path (non-checked) - rootDir = rootDirFile.getAbsolutePath(); - } - param.put(ROOT_DIR, rootDir); - } - - if (!outDirOrFileIsSet && errorMessage == null) { - errorMessage = Messages.NodeDebug_TSConfigError_OutDirIsNotSet; - } - - if (errorMessage != null) { - // Display error message - final int[] result = new int[1]; - final String dialogMessage = errorMessage; - final String editTSConfig = tsConfigFile.exists() && tsConfigFile.isFile() ? - Messages.NodeDebug_TSConfirError_OpenTSConfigInEditor : - Messages.NodeDebug_TSConfirError_CreateAndOpenTSConfigInEditor; - final File directory = parentDirectory; - - Display.getDefault().syncExec(() -> { - MessageDialog dialog = new MessageDialog(DebugUIPlugin.getShell(), - Messages.NodeDebug_TSConfirError_Title, null, dialogMessage, MessageDialog.QUESTION_WITH_CANCEL, - 2, editTSConfig, - Messages.NodeDebug_TSConfirError_StartDebuggingAsIs, Messages.NodeDebug_TSConfirError_Cancel); - result[0] = dialog.open(); - }); - - if (result[0] == 0) { - // Open TSConfig in editor - Display.getDefault().asyncExec(new Runnable() { - @Override - public void run() { - IFile file = createNewEmptyFile(new File(directory, TS_CONFIG_NAME)); - if (file != null) { - try { - IDE.openEditor( - PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), - new FileEditorInput(file), - "org.eclipse.ui.genericeditor.GenericEditor", - true); - } catch (PartInitException e1) { - Activator.getDefault().getLog().error(e1.getMessage(), e1); - } - } - } - - private IFile createNewEmptyFile(File fsFile) { - IWorkspace ws = ResourcesPlugin.getWorkspace(); - IWorkspaceRoot wr = ws.getRoot(); - IFile file = wr.getFileForLocation(new Path(fsFile.getAbsolutePath())); - if (!(file.exists() && file.isAccessible())) { - IFile[] result = new IFile[1]; - try { - ws.run((IWorkspaceRunnable) monitor -> { - result[0] = null; - try (ByteArrayInputStream is = new ByteArrayInputStream(new byte[0])) { - createContainers(file); - file.create(is, true, null); - file.refreshLocal(IResource.DEPTH_ZERO, null); - result[0] = file; - } catch (CoreException | IOException e) { - Activator.getDefault().getLog().error(e.getMessage(), e); - } - }, null); - } catch (CoreException e) { - Activator.getDefault().getLog().error(e.getMessage(), e); - } - return result[0]; - } - return file; - } - - void createContainers(IResource resource) throws CoreException { - IContainer container= resource.getParent(); - if (container instanceof IFolder parent && !parent.exists()) { - createContainers(parent); - parent.create(false, true, null); - } - } - }); - } else if (result[0] == 1) { - // Start debugging as is - return true; - } - return false; - } - - return true; - } else if (Platform.getContentTypeManager().getContentType(JAVACRIPT_CONTENT_TYPE) - .isAssociatedWith(programFile.getName())) { - - // JavaScript configuration - - // workaround until - // https://github.com/microsoft/vscode-node-debug2/commit/f2dfa4ca4026fb3e4f143a391270a03df8187b42#diff-d03a74f75ec189cbc7dd3d2e105fc9c9R625 - // is released in VSCode - param.put(SOURCE_MAPS, false); - param.put(JAVACRIPT_DEBUGGABLE_PATTERNS, JAVACRIPT_DEBUGGABLE_PATTERNS_DEFAULT); - - return true; - } - return false; - } - - private File findTSConfigFile(File parentDirectory) { - File tsConfigFile; - do { - tsConfigFile = new File(parentDirectory, TS_CONFIG_NAME); - if (tsConfigFile.isFile()) { - return tsConfigFile; - } - parentDirectory = parentDirectory.getParentFile(); - } while (parentDirectory != null && parentDirectory.isDirectory()); - return null; - } - - private static final Pattern BlockCommentPattern = Pattern.compile("(? readJSonFile(File tsConfgFile) { - if (tsConfgFile == null || !tsConfgFile.isFile()) { - return Map.of(); - } - try (BufferedReader in = new BufferedReader(new FileReader(tsConfgFile))) { - String inputLine; - StringBuffer response = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - response.append(inputLine).append('\n'); - } - Type type = new TypeToken>() {}.getType(); - return new Gson().fromJson(getSanitisedTSConfigForGson(response.toString()), type); - } catch (IOException e) { - return Map.of(); - } + public NodeRunDAPDebugDelegate() { + super("pwa-node"); } } diff --git a/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/VSCodeJSDebugDelegate.java b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/VSCodeJSDebugDelegate.java new file mode 100644 index 0000000000..358799c41f --- /dev/null +++ b/org.eclipse.wildwebdeveloper/src/org/eclipse/wildwebdeveloper/debug/node/VSCodeJSDebugDelegate.java @@ -0,0 +1,430 @@ +/******************************************************************************* + * Copyright (c) 2018, 2023 Red Hat Inc. 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 + * + * Contributors: + * Mickael Istria (Red Hat Inc.) - initial implementation + * Pierre-Yves B. - Issue #180 Wrong path to nodeDebug.js + *******************************************************************************/ +package org.eclipse.wildwebdeveloper.debug.node; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.ServerSocket; +import java.net.URL; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import org.eclipse.core.resources.IContainer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.IWorkspaceRunnable; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.variables.VariablesPlugin; +import org.eclipse.debug.core.DebugEvent; +import org.eclipse.debug.core.DebugException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.IDebugEventSetListener; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchManager; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.core.model.IProcess; +import org.eclipse.debug.internal.ui.DebugUIPlugin; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.lsp4e.LanguageServerPlugin; +import org.eclipse.lsp4e.debug.DSPPlugin; +import org.eclipse.lsp4e.debug.launcher.DSPLaunchDelegate; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.FileEditorInput; +import org.eclipse.wildwebdeveloper.Activator; +import org.eclipse.wildwebdeveloper.debug.LaunchConstants; +import org.eclipse.wildwebdeveloper.debug.Messages; +import org.eclipse.wildwebdeveloper.embedder.node.NodeJSManager; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +/** + * A generic LaunchDelegate for vscode-js-debug adapters + */ +@SuppressWarnings("restriction") +public abstract class VSCodeJSDebugDelegate extends DSPLaunchDelegate { + + // see https://github.com/Microsoft/vscode-node-debug/blob/master/src/node/nodeDebug.ts LaunchRequestArguments + public static final String ARGUMENTS = "args"; //$NON-NLS-1$ + private static final String CWD = "cwd"; //$NON-NLS-1$ + private static final String ENV = "env"; //$NON-NLS-1$ + protected static final String RUNTIME_EXECUTABLE = "runtimeExecutable"; //$NON-NLS-1$ + + public static final String NODE_DEBUG_CMD = "/js-debug/src/dapDebugServer.js"; //$NON-NLS-1$ + public static final String TYPESCRIPT_CONTENT_TYPE = "org.eclipse.wildwebdeveloper.ts"; //$NON-NLS-1$ + public static final String JAVACRIPT_CONTENT_TYPE = "org.eclipse.wildwebdeveloper.js"; //$NON-NLS-1$ + + public static final String JAVACRIPT_DEBUGGABLE_PATTERNS = "__debuggablePatterns"; + public static final String JAVACRIPT_DEBUGGABLE_PATTERNS_DEFAULT = "[\"*.js\",\"*.es6\",\"*.jsx\",\"*.mjs\".\"*.cjs\"]"; + + + private static final String TS_CONFIG_NAME = "tsconfig.json"; //$NON-NLS-1$ + private static final String COMPILER_OPTIONS = "compilerOptions"; //$NON-NLS-1$ + private static final String SOURCE_MAPS = "sourceMaps"; //$NON-NLS-1$ + private static final String OUT_DIR = "outDir"; //$NON-NLS-1$ + private static final String ROOT_DIR = "rootDir"; //$NON-NLS-1$ + + protected final String type; + + protected VSCodeJSDebugDelegate(String type) { + this.type = type; + } + + @Override + public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) + throws CoreException { + // user settings + Map param = new HashMap<>(); + String program = VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(configuration.getAttribute(LaunchConstants.PROGRAM, "no program path defined")); //$NON-NLS-1$ + param.put(LaunchConstants.PROGRAM, program); + param.put("type", type); + param.put("request", "launch"); + String argsString = configuration.getAttribute(ARGUMENTS, "").trim(); //$NON-NLS-1$ + if (!argsString.isEmpty()) { + Object[] args = Arrays.asList(argsString.split(" ")).stream() //$NON-NLS-1$ + .filter(s -> !s.trim().isEmpty()) + .map(s -> { + try { + return VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(s); + } catch (CoreException e) { + IStatus errorStatus = new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e); + Activator.getDefault().getLog().log(errorStatus); + return s; + } + }) + .toArray(); + if (args.length > 0) { + param.put(ARGUMENTS, args); + } + } + Map env = configuration.getAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, + Collections.emptyMap()); + if (!env.isEmpty()) { + JsonObject envJson = new JsonObject(); + for (Entry entry : env.entrySet()) { + envJson.addProperty(entry.getKey(), VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(entry.getValue())); + } + param.put(ENV, envJson); + } + String cwd = configuration.getAttribute(DebugPlugin.ATTR_WORKING_DIRECTORY, "").trim(); //$NON-NLS-1$ + param.put(CWD, cwd.isEmpty() ? new File(program).getParentFile().getAbsolutePath() : VariablesPlugin.getDefault().getStringVariableManager().performStringSubstitution(cwd)); + File runtimeExecutable = NodeJSManager.getNodeJsLocation(); + if (runtimeExecutable != null) { + param.put(RUNTIME_EXECUTABLE, runtimeExecutable.getAbsolutePath()); + } + + if (!configureAdditionalParameters(param)) { + return; + } + + try { + URL fileURL = FileLocator.toFileURL(getClass().getResource(NODE_DEBUG_CMD)); + File file = new File(fileURL.getPath()); + int port = 0; + try (ServerSocket serverSocket = new ServerSocket(0)) { + port = serverSocket.getLocalPort(); + serverSocket.close(); + } catch (IOException ex) { + Activator.getDefault().getLog().log(Status.error(ex.getMessage(), ex)); + } + File cwdFile = cwd == null || cwd.isBlank() ? new File(System.getProperty("user.dir")) : new File(cwd); //$NON-NLS-1$ + Process vscodeJsDebugExec = DebugPlugin.exec(new String[] { runtimeExecutable.getAbsolutePath(), file.getAbsolutePath(), Integer.toString(port) }, cwdFile, new String[] { "DA_TEST_DISABLE_TELEMETRY=true"}, false); + IProcess vscodeJsDebugIProcess = DebugPlugin.newProcess(launch, vscodeJsDebugExec, "debug adapter"); + AtomicBoolean started = new AtomicBoolean(); + vscodeJsDebugIProcess.getStreamsProxy().getOutputStreamMonitor().addListener((text, mon) -> { + if (text.toLowerCase().contains("listening")) { + started.set(true); + } + }); + Instant request = Instant.now(); + while (!started.get() && Duration.between(request, Instant.now()).compareTo(Duration.ofSeconds(3)) < 3) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + + DSPLaunchDelegateLaunchBuilder builder = new DSPLaunchDelegateLaunchBuilder(configuration, mode, launch, + monitor); + builder.setAttachDebugAdapter("::1", port); + builder.setMonitorDebugAdapter(configuration.getAttribute(DSPPlugin.ATTR_DSP_MONITOR_DEBUG_ADAPTER, false)); + builder.setDspParameters(param); + super.launch(builder); + IDebugEventSetListener shutdownParentOnCompletion = new IDebugEventSetListener() { + @Override + public void handleDebugEvents(DebugEvent[] events) { + if (Arrays.stream(events).anyMatch(event -> + event.getKind() == DebugEvent.TERMINATE && + event.getSource() instanceof final IDebugTarget target && + target.getLaunch() == launch)) { + if (Arrays.stream(launch.getDebugTargets()).allMatch(IDebugTarget::isTerminated) + && List.of(vscodeJsDebugIProcess).equals(Arrays.stream(launch.getProcesses()).filter(Predicate.not(IProcess::isTerminated)).toList())) { + try { + vscodeJsDebugIProcess.terminate(); + } catch (DebugException ex) { + vscodeJsDebugExec.destroy(); + } + DebugPlugin.getDefault().removeDebugEventListener(this); + } + } + } + }; + DebugPlugin.getDefault().addDebugEventListener(shutdownParentOnCompletion); + } catch (IOException ex) { + LanguageServerPlugin.logError(ex); + } + } + + private boolean configureAdditionalParameters(Map param) { + String program = (String)param.get(LaunchConstants.PROGRAM); + String cwd = (String)param.get(CWD); + + if (program == null) { + return false; + } + + File programFile = new File(program); + if (Platform.getContentTypeManager().getContentType(TYPESCRIPT_CONTENT_TYPE) + .isAssociatedWith(programFile.getName())) { + // TypeScript Source Mappings Configuration + param.put(SOURCE_MAPS, true); + File parentDirectory = cwd == null ? programFile.getParentFile() : new File(cwd); + File tsConfigFile = findTSConfigFile(parentDirectory); + if (tsConfigFile != null && tsConfigFile.exists()) { + parentDirectory = tsConfigFile.getParentFile(); + } + + String errorMessage = null; + Map tsConfig = readJSonFile(tsConfigFile); + + @SuppressWarnings("unchecked") + Map co = tsConfig == null ? null : (Map)tsConfig.get(COMPILER_OPTIONS); + if (co == null) { + errorMessage = Messages.NodeDebug_TSConfirError_NoTsConfig; + co = new HashMap<>(); + } + + //TS Compiler Options + //param.putAll(co); + + // Override "outDir" option by converting it to an absolute path + boolean outDirOrFileIsSet = false; + + String outDir = co.get(OUT_DIR) instanceof String o ? o.trim() : null; + if (outDir != null && outDir.length() > 0 && !".".equals(outDir) && !"./".equals(outDir)) { + File outDirFile = new File(parentDirectory, outDir); + try { + outDir = outDirFile.getCanonicalPath(); + } catch (IOException e) { + // Default to an absolute file path (non-checked) + outDir = outDirFile.getAbsolutePath(); + } + outDirOrFileIsSet = true; + param.put("outFiles", List.of(outDirFile.getAbsolutePath() + "/**/*.js")); + Path jsFile = outDirFile.toPath().resolve(tsConfigFile.getParentFile().toPath().relativize(programFile.toPath().getParent().resolve(toJS(programFile.getName())))); + param.put("program", jsFile.toString()); + } + + param.put("rootPath", tsConfigFile.getParentFile().getAbsolutePath()); + String rootDir = co.get(ROOT_DIR) instanceof String o ? o.trim() : null; + if (rootDir != null && rootDir.length() > 0 && !".".equals(outDir) && !"./".equals(outDir)) { + File rootDirFile = new File(parentDirectory, rootDir); + try { + rootDir = rootDirFile.getCanonicalPath(); + } catch (IOException e) { + // Default to an absolute file path (non-checked) + rootDir = rootDirFile.getAbsolutePath(); + } + param.put(ROOT_DIR, rootDir); + param.put("rootPath", rootDir); + } + + if (!outDirOrFileIsSet && errorMessage == null) { + errorMessage = Messages.NodeDebug_TSConfigError_OutDirIsNotSet; + } + + if (errorMessage != null) { + // Display error message + final int[] result = new int[1]; + final String dialogMessage = errorMessage; + final String editTSConfig = tsConfigFile.exists() && tsConfigFile.isFile() ? + Messages.NodeDebug_TSConfirError_OpenTSConfigInEditor : + Messages.NodeDebug_TSConfirError_CreateAndOpenTSConfigInEditor; + final File directory = parentDirectory; + + Display.getDefault().syncExec(() -> { + MessageDialog dialog = new MessageDialog(DebugUIPlugin.getShell(), + Messages.NodeDebug_TSConfirError_Title, null, dialogMessage, MessageDialog.QUESTION_WITH_CANCEL, + 2, editTSConfig, + Messages.NodeDebug_TSConfirError_StartDebuggingAsIs, Messages.NodeDebug_TSConfirError_Cancel); + result[0] = dialog.open(); + }); + + if (result[0] == 0) { + // Open TSConfig in editor + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + IFile file = createNewEmptyFile(new File(directory, TS_CONFIG_NAME)); + if (file != null) { + try { + IDE.openEditor( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), + new FileEditorInput(file), + "org.eclipse.ui.genericeditor.GenericEditor", + true); + } catch (PartInitException e1) { + Activator.getDefault().getLog().error(e1.getMessage(), e1); + } + } + } + + private IFile createNewEmptyFile(File fsFile) { + IWorkspace ws = ResourcesPlugin.getWorkspace(); + IWorkspaceRoot wr = ws.getRoot(); + IFile file = wr.getFileForLocation(IPath.fromFile(fsFile)); + if (!(file.exists() && file.isAccessible())) { + IFile[] result = new IFile[1]; + try { + ws.run((IWorkspaceRunnable) monitor -> { + result[0] = null; + try (ByteArrayInputStream is = new ByteArrayInputStream(new byte[0])) { + createContainers(file); + file.create(is, true, null); + file.refreshLocal(IResource.DEPTH_ZERO, null); + result[0] = file; + } catch (CoreException | IOException e) { + Activator.getDefault().getLog().error(e.getMessage(), e); + } + }, null); + } catch (CoreException e) { + Activator.getDefault().getLog().error(e.getMessage(), e); + } + return result[0]; + } + return file; + } + + void createContainers(IResource resource) throws CoreException { + IContainer container= resource.getParent(); + if (container instanceof IFolder parent && !parent.exists()) { + createContainers(parent); + parent.create(false, true, null); + } + } + }); + } else if (result[0] == 1) { + // Start debugging as is + return true; + } + return false; + } + + return true; + } else if (Platform.getContentTypeManager().getContentType(JAVACRIPT_CONTENT_TYPE) + .isAssociatedWith(programFile.getName())) { + + // JavaScript configuration + + // workaround until + // https://github.com/microsoft/vscode-node-debug2/commit/f2dfa4ca4026fb3e4f143a391270a03df8187b42#diff-d03a74f75ec189cbc7dd3d2e105fc9c9R625 + // is released in VSCode + param.put(SOURCE_MAPS, false); + param.put(JAVACRIPT_DEBUGGABLE_PATTERNS, JAVACRIPT_DEBUGGABLE_PATTERNS_DEFAULT); + + return true; + } + return false; + } + + private String toJS(String name) { + return name.endsWith(".js") ? name : name.substring(0, name.length() - 2) + "js"; + } + + private File findTSConfigFile(File parentDirectory) { + File tsConfigFile; + do { + tsConfigFile = new File(parentDirectory, TS_CONFIG_NAME); + if (tsConfigFile.isFile()) { + return tsConfigFile; + } + parentDirectory = parentDirectory.getParentFile(); + } while (parentDirectory != null && parentDirectory.isDirectory()); + return null; + } + + private static final Pattern BlockCommentPattern = Pattern.compile("(? readJSonFile(File tsConfgFile) { + if (tsConfgFile == null || !tsConfgFile.isFile()) { + return Map.of(); + } + try (BufferedReader in = new BufferedReader(new FileReader(tsConfgFile))) { + String inputLine; + StringBuffer response = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine).append('\n'); + } + Type type = new TypeToken>() {}.getType(); + return new Gson().fromJson(getSanitisedTSConfigForGson(response.toString()), type); + } catch (IOException e) { + return Map.of(); + } + } +} diff --git a/target-platform/target-platform.target b/target-platform/target-platform.target index 503c6fe288..e3e4d06f63 100644 --- a/target-platform/target-platform.target +++ b/target-platform/target-platform.target @@ -41,7 +41,9 @@ - + + +