diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/BuildToolDelegate.java b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/BuildToolDelegate.java index 8679e0440..37b04ee03 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/BuildToolDelegate.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/BuildToolDelegate.java @@ -10,6 +10,7 @@ ******************************************************************************/ package com.redhat.devtools.intellij.quarkus.buildtool; +import com.intellij.execution.Executor; import com.intellij.execution.RunnerAndConfigurationSettings; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.module.Module; @@ -21,13 +22,10 @@ import com.intellij.util.messages.MessageBusConnection; import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.maven.model.MavenId; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; +import java.io.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -64,18 +62,20 @@ static String getDeploymentJarId(File file) { if (quarkusFile.exists()) { try (Reader r = new FileReader(quarkusFile)) { result = getQuarkusExtension(r); - } catch (IOException e) {} + } catch (IOException e) { + } } } else { try { JarFile jarFile = new JarFile(file); JarEntry entry = jarFile.getJarEntry(QUARKUS_EXTENSION_PROPERTIES); if (entry != null) { - try (Reader r = new InputStreamReader(jarFile.getInputStream(entry),"UTF-8")) { + try (Reader r = new InputStreamReader(jarFile.getInputStream(entry), "UTF-8")) { result = getQuarkusExtension(r); } } - } catch (IOException e) {} + } catch (IOException e) { + } } return result; } @@ -91,7 +91,7 @@ static String getQuarkusExtension(Reader r) throws IOException { } public static BuildToolDelegate getDelegate(Module module) { - for(BuildToolDelegate toolDelegate : getDelegates()) { + for (BuildToolDelegate toolDelegate : getDelegates()) { if (toolDelegate.isValid(module)) { return toolDelegate; } @@ -160,7 +160,7 @@ default VirtualFile getJarFile(String path) { default VirtualFile getJarFile(File file) { VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); - return virtualFile != null? JarFileSystem.getInstance().getJarRootForLocalFile(virtualFile):null; + return virtualFile != null ? JarFileSystem.getInstance().getJarRootForLocalFile(virtualFile) : null; } static final ExtensionPointName EP_NAME = ExtensionPointName.create("com.redhat.devtools.intellij.quarkus.toolDelegate"); @@ -175,18 +175,40 @@ static List[] initDeploymentFiles() { public static BuildToolDelegate[] getDelegates() { BuildToolDelegate[] delegates = EP_NAME.getExtensions(); - Arrays.sort(delegates, (a,b) -> a.getOrder() - b.getOrder()); + Arrays.sort(delegates, (a, b) -> a.getOrder() - b.getOrder()); return delegates; } - RunnerAndConfigurationSettings getConfigurationDelegate(Module module, QuarkusRunConfiguration configuration); + /** + * Returns the configuration delegate (Gradle, Maven) according the given the module and quarkus configuration + * and null otherwise. + * + * @param module the module. + * @param configuration the quarkus configuration. + * @param debugPort the debug port tu use if Quarkus application must be debugged and null otherwise. + * @return the configuration delegate (Gradle,Maven). + */ + @Nullable + RunnerAndConfigurationSettings getConfigurationDelegate(@NotNull Module module, + @NotNull QuarkusRunConfiguration configuration, + @Nullable Integer debugPort); /** * Add project import listener. * - * @param project the project. + * @param project the project. * @param connection the project connection used to subscribe maven, gradle listener which tracks project import. - * @param listener the project import listener. + * @param listener the project import listener. */ - void addProjectImportListener(@NotNull Project project, @NotNull MessageBusConnection connection, @NotNull ProjectImportListener listener); + void addProjectImportListener(@NotNull Project project, @NotNull MessageBusConnection connection, @NotNull ProjectImportListener listener); + + /** + * Returns the override executor and null otherwise. + * + * @return the override executor and null otherwise. + */ + @Nullable + default Executor getOverridedExecutor() { + return null; + } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/gradle/AbstractGradleToolDelegate.java b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/gradle/AbstractGradleToolDelegate.java index fb4e7f9cf..67dd8afdd 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/gradle/AbstractGradleToolDelegate.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/gradle/AbstractGradleToolDelegate.java @@ -10,6 +10,8 @@ ******************************************************************************/ package com.redhat.devtools.intellij.quarkus.buildtool.gradle; +import com.intellij.execution.Executor; +import com.intellij.execution.ExecutorRegistry; import com.intellij.execution.RunManager; import com.intellij.execution.RunnerAndConfigurationSettings; import com.intellij.ide.util.newProjectWizard.AddModuleWizard; @@ -38,6 +40,7 @@ import com.intellij.openapi.roots.ui.configuration.ModulesProvider; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.ToolWindowId; import com.intellij.projectImport.ProjectImportBuilder; import com.intellij.projectImport.ProjectImportProvider; import com.intellij.util.concurrency.NonUrgentExecutor; @@ -358,12 +361,14 @@ private ProjectImportProvider getGradleProjectImportProvider() { } @Override - public RunnerAndConfigurationSettings getConfigurationDelegate(Module module, QuarkusRunConfiguration configuration) { + public RunnerAndConfigurationSettings getConfigurationDelegate(@NotNull Module module, + @NotNull QuarkusRunConfiguration configuration, + @Nullable Integer debugPort) { RunnerAndConfigurationSettings settings = RunManager.getInstance(module.getProject()).createConfiguration(module.getName() + " Quarkus (Gradle)", GradleExternalTaskConfigurationType.class); GradleRunConfiguration gradleConfiguration = (GradleRunConfiguration) settings.getConfiguration(); gradleConfiguration.getSettings().getTaskNames().add("quarkusDev"); gradleConfiguration.getSettings().setEnv(configuration.getEnv()); - String parameters = "-Ddebug=" + configuration.getPort(); + String parameters = debugPort != null ? ("-Ddebug=" + Integer.toString(debugPort)) : ""; if (StringUtils.isNotBlank(configuration.getProfile())) { parameters += " -Dquarkus.profile=" + configuration.getProfile(); } @@ -407,4 +412,12 @@ private static boolean isValidGradleModule(Module module) { String name = module.getName(); return !(name.endsWith(".integrationTest") || name.endsWith(".native-test") || name.endsWith(".test")); } + + @Override + public @Nullable Executor getOverridedExecutor() { + // The run and debug gradle must be started with the DefaultRunExecutor + // and not with the DefaultDebugExecutor in debug case otherwise + // stop action doesn't kill the Quarkus application process. + return ExecutorRegistry.getInstance().getExecutorById(ToolWindowId.RUN); + } } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/gradle/GradleRunAndDebugProgramRunner.java b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/gradle/GradleRunAndDebugProgramRunner.java new file mode 100644 index 000000000..a7c1a9208 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/gradle/GradleRunAndDebugProgramRunner.java @@ -0,0 +1,112 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.redhat.devtools.intellij.quarkus.buildtool.gradle; + +import com.intellij.build.BuildView; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.ExecutionManager; +import com.intellij.execution.ExecutionResult; +import com.intellij.execution.configurations.RunProfile; +import com.intellij.execution.configurations.RunProfileState; +import com.intellij.execution.configurations.RunnerSettings; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.execution.runners.ProgramRunner; +import com.intellij.execution.runners.RunContentBuilder; +import com.intellij.execution.testframework.HistoryTestRunnableState; +import com.intellij.execution.ui.RunContentDescriptor; +import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunnableState; +import com.redhat.devtools.intellij.quarkus.buildtool.BuildToolDelegate; +import com.redhat.devtools.intellij.quarkus.buildtool.maven.MavenToolDelegate; +import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration; +import org.jetbrains.concurrency.Promises; + +/** + * Program runner to run/debug a Gradle configuration. + *

+ * This class is a copy/paste from the Intellij + * ExternalSystemTaskRunner + * since this class cannot be extended. + */ +public class GradleRunAndDebugProgramRunner implements ProgramRunner { + + private static final String RUNNER_ID = "GradleRunAndDebugProgramRunner"; + + @Override + public String getRunnerId() { + return RUNNER_ID; + } + + @Override + public boolean canRun(String executorId, RunProfile profile) { + // For running / debugging Gradle 'quarkusDev', the program runner must be executed + // with the standard IJ 'ExternalSystemTaskRunner' which works only if + // the profile is an 'ExternalSystemRunConfiguration'. As QuarkusRunConfiguration + // wraps the profile, this condition is not matched. + // GradleRunAndDebugProgramRunner should extend ExternalSystemTaskRunner but as this class + // is final, GradleRunAndDebugProgramRunner is a copy/paste of ExternalSystemTaskRunner + // and the profile check is done by checking if QuarkusRunConfiguration wraps a Gradle run/debug configuration. + if (profile instanceof QuarkusRunConfiguration quarkusRunConfiguration) { + // returns true if the profile is a QuarkusRunConfiguration which wraps a Gradle configuration + BuildToolDelegate delegate = BuildToolDelegate.getDelegate(quarkusRunConfiguration.getModule()); + return !(delegate instanceof MavenToolDelegate); + } + return false; + } + + @Override + public void execute(ExecutionEnvironment environment) throws ExecutionException { + RunProfileState state = environment.getState(); + if (state == null) { + return; + } + ExecutionManager.getInstance(environment.getProject()).startRunProfile(environment, () -> { + try { + return Promises.resolvedPromise(doExecute(state, environment)); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } + }); + } + + private RunContentDescriptor doExecute(RunProfileState state, ExecutionEnvironment environment) throws ExecutionException { + if (!(state instanceof ExternalSystemRunnableState) && !(state instanceof HistoryTestRunnableState)) { + return null; + } + + RunContentDescriptor runContentDescriptor; + ExecutionResult executionResult = state.execute(environment.getExecutor(), this); + if (executionResult == null) { + return null; + } + runContentDescriptor = new RunContentBuilder(executionResult, environment).showRunContent(environment.getContentToReuse()); + if (runContentDescriptor == null) { + return null; + } + + if (state instanceof HistoryTestRunnableState) { + return runContentDescriptor; + } + + ((ExternalSystemRunnableState) state).setContentDescriptor(runContentDescriptor); + + if (executionResult.getExecutionConsole() instanceof BuildView) { + return runContentDescriptor; + } + + RunContentDescriptor descriptor = new RunContentDescriptor( + runContentDescriptor.getExecutionConsole(), + runContentDescriptor.getProcessHandler(), + runContentDescriptor.getComponent(), + runContentDescriptor.getDisplayName(), + runContentDescriptor.getIcon(), + null, + runContentDescriptor.getRestartActions() + ) { + @Override + public boolean isHiddenContent() { + return true; + } + }; + descriptor.setRunnerLayoutUi(runContentDescriptor.getRunnerLayoutUi()); + return descriptor; + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/MavenToolDelegate.java b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/MavenToolDelegate.java index b6b61d15d..c988a7572 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/MavenToolDelegate.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/MavenToolDelegate.java @@ -28,6 +28,7 @@ import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.maven.execution.MavenRunConfiguration; import org.jetbrains.idea.maven.execution.MavenRunConfigurationType; import org.jetbrains.idea.maven.execution.MavenRunnerSettings; @@ -39,7 +40,6 @@ import org.jetbrains.idea.maven.project.MavenProject; import org.jetbrains.idea.maven.project.MavenProjectsManager; import org.jetbrains.idea.maven.server.MavenEmbedderWrapper; -import org.jetbrains.idea.maven.utils.MavenProcessCanceledException; import org.jetbrains.idea.maven.utils.MavenUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -260,7 +260,7 @@ private MavenEmbedderWrapper createEmbedderWrapper(Project project, String worki } @Override - public RunnerAndConfigurationSettings getConfigurationDelegate(Module module, QuarkusRunConfiguration configuration) { + public RunnerAndConfigurationSettings getConfigurationDelegate(Module module, QuarkusRunConfiguration configuration, @Nullable Integer debugPort) { RunnerAndConfigurationSettings settings = RunManager.getInstance(module.getProject()).createConfiguration(module.getName() + " Quarkus (Maven)", MavenRunConfigurationType.class); MavenRunConfiguration mavenConfiguration = (MavenRunConfiguration) settings.getConfiguration(); mavenConfiguration.getRunnerParameters().setResolveToWorkspace(true); @@ -271,7 +271,9 @@ public RunnerAndConfigurationSettings getConfigurationDelegate(Module module, Qu if (StringUtils.isNotBlank(configuration.getProfile())) { mavenConfiguration.getRunnerSettings().getMavenProperties().put("quarkus.profile", configuration.getProfile()); } - mavenConfiguration.getRunnerSettings().getMavenProperties().put("debug", Integer.toString(configuration.getPort())); + if (debugPort != null) { + mavenConfiguration.getRunnerSettings().getMavenProperties().put("debug", Integer.toString(debugPort)); + } mavenConfiguration.setBeforeRunTasks(configuration.getBeforeRunTasks()); return settings; } diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/QuarkusMavenDebugProgramRunner.java b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/QuarkusMavenDebugProgramRunner.java new file mode 100644 index 000000000..282c70340 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/QuarkusMavenDebugProgramRunner.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2024 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, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.buildtool.maven; + +import com.intellij.debugger.impl.GenericDebuggerRunner; +import com.intellij.execution.configurations.RunProfile; +import com.intellij.execution.executors.DefaultDebugExecutor; +import com.redhat.devtools.intellij.quarkus.buildtool.BuildToolDelegate; +import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration; +import org.jetbrains.annotations.NotNull; + +/** + * Program runner to debug a Maven configuration. + */ +public class QuarkusMavenDebugProgramRunner extends GenericDebuggerRunner { + + private static final String RUNNER_ID = "QuarkusMavenDebugProgramRunner"; + + @Override + public String getRunnerId() { + return RUNNER_ID; + } + + @Override + public boolean canRun(@NotNull final String executorId, @NotNull final RunProfile profile) { + if (!executorId.equals(DefaultDebugExecutor.EXECUTOR_ID)) { + return false; + } + // Debuging... + if (profile instanceof QuarkusRunConfiguration quarkusRunConfiguration) { + // returns true if the profile is a QuarkusRunConfiguration which wraps a Maven configuration + BuildToolDelegate delegate = BuildToolDelegate.getDelegate(quarkusRunConfiguration.getModule()); + return (delegate instanceof MavenToolDelegate); + } + return false; + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/QuarkusMavenRunProgramRunner.java b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/QuarkusMavenRunProgramRunner.java new file mode 100644 index 000000000..cebc3f047 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/buildtool/maven/QuarkusMavenRunProgramRunner.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2024 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, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.buildtool.maven; + +import com.intellij.execution.configurations.RunProfile; +import com.intellij.execution.executors.DefaultRunExecutor; +import com.intellij.execution.impl.DefaultJavaProgramRunner; +import com.redhat.devtools.intellij.quarkus.buildtool.BuildToolDelegate; +import com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration; + +/** + * Program runner to run a Maven configuration. + */ +public class QuarkusMavenRunProgramRunner extends DefaultJavaProgramRunner { + + private static final String RUNNER_ID = "QuarkusMavenRunProgramRunner"; + + @Override + public String getRunnerId() { + return RUNNER_ID; + } + + @Override + public boolean canRun(String executorId, RunProfile profile) { + if (!executorId.equals(DefaultRunExecutor.EXECUTOR_ID)) { + return false; + } + // Running... + if (profile instanceof QuarkusRunConfiguration quarkusRunConfiguration) { + // returns true if the profile is a QuarkusRunConfiguration which wraps a Maven configuration + BuildToolDelegate delegate = BuildToolDelegate.getDelegate(quarkusRunConfiguration.getModule()); + return (delegate instanceof MavenToolDelegate); + } + return false; + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/AttachDebuggerExecutionListener.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/AttachDebuggerExecutionListener.java new file mode 100644 index 000000000..91622b2be --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/AttachDebuggerExecutionListener.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2024 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, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.run; + +import com.intellij.execution.ExecutionListener; +import com.intellij.execution.RunnerAndConfigurationSettings; +import com.intellij.execution.executors.DefaultDebugExecutor; +import com.intellij.execution.process.ProcessHandler; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +/** + * Execution listener singleton which tracks any process starting to add in debug mode + * an instance of {@link AttachDebuggerProcessListener} to the process handler + * if it is a Quarkus run configuration. + */ +class AttachDebuggerExecutionListener implements ExecutionListener { + + private final @NotNull Project project; + + AttachDebuggerExecutionListener(@NotNull Project project) { + this.project = project; + } + + public void processStarting(@NotNull String executorId, + @NotNull ExecutionEnvironment env, + @NotNull ProcessHandler handler) { + if (!DefaultDebugExecutor.EXECUTOR_ID.equals(executorId)) { + return; + } + // Debug mode... + RunnerAndConfigurationSettings settings = env.getRunnerAndConfigurationSettings(); + if (settings != null && settings.getConfiguration() instanceof QuarkusRunConfiguration) { + // The execution has been done by debugging a Quarkus run configuration (Gradle / Maven) + // add a AttachDebuggerProcessListener to track + // 'Listening for transport dt_socket at address: $PORT' message and starts + // the remote debugger with the givenport $PORT + handler.addProcessListener(new AttachDebuggerProcessListener(project, env)); + } + } + +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/AttachDebuggerProcessListener.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/AttachDebuggerProcessListener.java new file mode 100644 index 000000000..7a5c27b75 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/AttachDebuggerProcessListener.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2024 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, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.intellij.quarkus.run; + +import com.intellij.execution.DefaultExecutionTarget; +import com.intellij.execution.RunManager; +import com.intellij.execution.RunnerAndConfigurationSettings; +import com.intellij.execution.executors.DefaultDebugExecutor; +import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.process.ProcessListener; +import com.intellij.execution.remote.RemoteConfiguration; +import com.intellij.execution.remote.RemoteConfigurationType; +import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.execution.runners.ExecutionUtil; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Key; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.ConnectException; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +import static com.redhat.devtools.intellij.quarkus.run.QuarkusRunConfiguration.QUARKUS_CONFIGURATION; + +class AttachDebuggerProcessListener implements ProcessListener { + + private final static Logger LOGGER = LoggerFactory.getLogger(AttachDebuggerProcessListener.class); + + private static final String LISTENING_FOR_TRANSPORT_DT_SOCKET_AT_ADDRESS = "Listening for transport dt_socket at address: "; + + private static final String JWDP_HANDSHAKE = "JDWP-Handshake"; + + private final Project project; + private final ExecutionEnvironment env; + private boolean connected; // to prevent from several messages like 'Listening for transport dt_socket at address:' + + AttachDebuggerProcessListener(@NotNull Project project, + @NotNull ExecutionEnvironment env) { + this.project = project; + this.env = env; + } + + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { + String message = event.getText(); + if (!connected && message.startsWith(LISTENING_FOR_TRANSPORT_DT_SOCKET_AT_ADDRESS)) { + connected = true; + Integer debugPort = getDebugPort(message); + if (debugPort == null) { + LOGGER.error("Cannot extract port from the given message: " + message); + return; + } + ProgressManager.getInstance().run(new Task.Backgroundable(project, QUARKUS_CONFIGURATION, false) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + String name = env.getRunProfile().getName(); + createRemoteConfiguration(indicator, debugPort, name); + } + }); + } + } + + @Nullable + private static Integer getDebugPort(String message) { + try { + String port = message.substring(LISTENING_FOR_TRANSPORT_DT_SOCKET_AT_ADDRESS.length(), message.length()).trim(); + return Integer.valueOf(port); + } catch (Exception e) { + return null; + } + } + + @Override + public void processTerminated(@NotNull ProcessEvent event) { + event.getProcessHandler().removeProcessListener(this); + } + + private void createRemoteConfiguration(ProgressIndicator indicator, int port, String name) { + indicator.setText("Connecting Java debugger to port " + port); + try { + waitForPortAvailable(port, indicator); + RunnerAndConfigurationSettings settings = RunManager.getInstance(project).createConfiguration(name + " (Remote)", RemoteConfigurationType.class); + RemoteConfiguration remoteConfiguration = (RemoteConfiguration) settings.getConfiguration(); + remoteConfiguration.PORT = Integer.toString(port); + long groupId = ExecutionEnvironment.getNextUnusedExecutionId(); + ExecutionUtil.runConfiguration(settings, DefaultDebugExecutor.getDebugExecutorInstance(), DefaultExecutionTarget.INSTANCE, groupId); + } catch (IOException e) { + ApplicationManager.getApplication() + .invokeLater(() -> Messages.showErrorDialog("Can' t connector to port " + port, "Quarkus")); + } + } + + private void waitForPortAvailable(int port, ProgressIndicator monitor) throws IOException { + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start < 120_000 && !monitor.isCanceled()) { + try (Socket socket = new Socket("localhost", port)) { + socket.getOutputStream().write(JWDP_HANDSHAKE.getBytes(StandardCharsets.US_ASCII)); + return; + } catch (ConnectException e) { + try { + Thread.sleep(1000L); + } catch (InterruptedException e1) { + throw new IOException(e1); + } + } + } + throw new IOException("Can't connect remote debugger to port " + port); + } +} diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfiguration.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfiguration.java index fdadf6c1f..8d7df677d 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfiguration.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfiguration.java @@ -11,20 +11,20 @@ package com.redhat.devtools.intellij.quarkus.run; import com.intellij.execution.*; -import com.intellij.execution.configurations.ConfigurationFactory; -import com.intellij.execution.configurations.ModuleBasedConfiguration; -import com.intellij.execution.configurations.RunConfiguration; -import com.intellij.execution.configurations.RunConfigurationModule; -import com.intellij.execution.configurations.RunProfileState; -import com.intellij.execution.configurations.RuntimeConfigurationException; +import com.intellij.execution.configurations.*; import com.intellij.execution.executors.DefaultDebugExecutor; +import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.process.ProcessHandler; +import com.intellij.execution.process.ProcessListener; import com.intellij.execution.remote.RemoteConfiguration; import com.intellij.execution.remote.RemoteConfigurationType; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.runners.ExecutionEnvironmentBuilder; import com.intellij.execution.runners.ExecutionUtil; +import com.intellij.execution.runners.RunConfigurationWithSuppressedDefaultRunAction; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunnableState; import com.intellij.openapi.module.Module; import com.intellij.openapi.options.SettingsEditor; import com.intellij.openapi.progress.ProgressIndicator; @@ -32,6 +32,9 @@ import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.wm.ToolWindowId; +import com.intellij.util.net.NetUtils; import com.redhat.devtools.intellij.quarkus.QuarkusModuleUtil; import com.redhat.devtools.intellij.quarkus.buildtool.BuildToolDelegate; import com.redhat.devtools.intellij.quarkus.telemetry.TelemetryEventName; @@ -52,13 +55,17 @@ import static com.intellij.execution.runners.ExecutionUtil.createEnvironment; -public class QuarkusRunConfiguration extends ModuleBasedConfiguration { +/** + * Quarkus run configration which wraps Maven / Gradle configuration. + */ +public class QuarkusRunConfiguration extends ModuleBasedConfiguration + implements RunConfigurationWithSuppressedDefaultRunAction, RunConfigurationWithSuppressedDefaultDebugAction { + private final static Logger LOGGER = LoggerFactory.getLogger(QuarkusRunConfiguration.class); - private static final String QUARKUS_CONFIGURATION = "Quarkus Configuration"; - private int port = 5005; + static final String QUARKUS_CONFIGURATION = "Quarkus Configuration"; - private static final String JWDP_HANDSHAKE = "JDWP-Handshake"; + private static final int DEFAULT_PORT = 5005 ; public QuarkusRunConfiguration(Project project, ConfigurationFactory factory, String name) { super(name, getRunConfigurationModule(project), factory); @@ -111,12 +118,14 @@ public SettingsEditor getConfigurationEditor() { return new QuarkusRunSettingsEditor(getProject()); } - private void allocateLocalPort() { - try (ServerSocket socket = new ServerSocket(0)) { - port = socket.getLocalPort(); - } catch (IOException e) { - LOGGER.warn("Can't allocate a local port for this configuration", e); - } + private int allocateLocalPort() { + try { + return NetUtils.findAvailableSocketPort(); + } + catch (IOException e) { + LOGGER.warn("Unexpected I/O exception occurred on attempt to find a free port to use for external system task debugging", e); + } + return DEFAULT_PORT; } @Nullable @@ -128,13 +137,21 @@ public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEn telemetryData.put("kind", executor.getId()); BuildToolDelegate toolDelegate = BuildToolDelegate.getDelegate(module); - allocateLocalPort(); RunProfileState state = null; if (toolDelegate != null) { telemetryData.put("tool", toolDelegate.getDisplay()); + boolean debug = DefaultDebugExecutor.EXECUTOR_ID.equals(executor.getId()); + Integer debugPort = debug ? allocateLocalPort() : null; + // The parameter (run/debug) executor is filled according the run/debug action + // but in the case of Gradle, the executor must be only the run executor + // otherwise for some reason, the stop button will stop the task without stopping the Quarkus application process. + // Here we need to override the executor if Gradle is started in debug mode. + Executor overridedExecutor = toolDelegate.getOverridedExecutor(); + executor = overridedExecutor != null ? overridedExecutor : executor; // Create a Gradle or Maven run configuration in memory - RunnerAndConfigurationSettings settings = toolDelegate.getConfigurationDelegate(module, this); + RunnerAndConfigurationSettings settings = toolDelegate.getConfigurationDelegate(module, this, debugPort); if (settings != null) { + QuarkusRunConfigurationManager.getInstance(module.getProject()); // to be sure that Quarkus execution listener is registered long groupId = ExecutionEnvironment.getNextUnusedExecutionId(); state = doRunConfiguration(settings, executor, DefaultExecutionTarget.INSTANCE, groupId, null); } @@ -143,49 +160,9 @@ public RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEn } // Send "run-run" telemetry event TelemetryManager.instance().send(TelemetryEventName.RUN_RUN, telemetryData); - - if (executor.getId().equals(DefaultDebugExecutor.EXECUTOR_ID)) { - ProgressManager.getInstance().run(new Task.Backgroundable(getProject(), QUARKUS_CONFIGURATION, false) { - @Override - public void run(@NotNull ProgressIndicator indicator) { - createRemoteConfiguration(indicator); - } - }); - } return state; } - private void waitForPortAvailable(int port, ProgressIndicator monitor) throws IOException { - long start = System.currentTimeMillis(); - while (System.currentTimeMillis() - start < 60_000 && !monitor.isCanceled()) { - try (Socket socket = new Socket("localhost", port)) { - socket.getOutputStream().write(JWDP_HANDSHAKE.getBytes(StandardCharsets.US_ASCII)); - return; - } catch (ConnectException e) { - try { - Thread.sleep(1000L); - } catch (InterruptedException e1) { - throw new IOException(e1); - } - } - } - throw new IOException("Can't connect remote debuger to port " + port); - } - - private void createRemoteConfiguration(ProgressIndicator indicator) { - indicator.setText("Connecting Java debugger to port " + getPort()); - try { - waitForPortAvailable(getPort(), indicator); - RunnerAndConfigurationSettings settings = RunManager.getInstance(getProject()).createConfiguration(getName() + " (Remote)", RemoteConfigurationType.class); - RemoteConfiguration remoteConfiguration = (RemoteConfiguration) settings.getConfiguration(); - remoteConfiguration.PORT = Integer.toString(getPort()); - long groupId = ExecutionEnvironment.getNextUnusedExecutionId(); - ExecutionUtil.runConfiguration(settings, DefaultDebugExecutor.getDebugExecutorInstance(), DefaultExecutionTarget.INSTANCE, groupId); - } catch (IOException e) { - ApplicationManager.getApplication().invokeLater(() -> Messages.showErrorDialog("Can' t connector to port " + getPort(), "Quarkus")); - } - } - public String getProfile() { return getOptions().getProfile(); } @@ -207,10 +184,6 @@ public void setEnv(Map env) { getOptions().setEnv(env); } - public int getPort() { - return port; - } - private static RunProfileState doRunConfiguration(@NotNull RunnerAndConfigurationSettings configuration, @NotNull Executor executor, @Nullable ExecutionTarget targetOrNullForDefault, diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationManager.java b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationManager.java index 5d04f9b05..f127dd174 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationManager.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/run/QuarkusRunConfigurationManager.java @@ -13,6 +13,7 @@ *******************************************************************************/ package com.redhat.devtools.intellij.quarkus.run; +import com.intellij.execution.ExecutionManager; import com.intellij.execution.RunManager; import com.intellij.execution.RunnerAndConfigurationSettings; import com.intellij.execution.dashboard.RunDashboardManager; @@ -56,6 +57,7 @@ public static QuarkusRunConfigurationManager getInstance(Project project) { public QuarkusRunConfigurationManager(Project project) { this.project = project; connection = addProjectImportListener(project); + connection.subscribe(ExecutionManager.EXECUTION_TOPIC, new AttachDebuggerExecutionListener(project)); } public @Nullable RunnerAndConfigurationSettings findExistingConfigurationFor(@NotNull Module module) { diff --git a/src/main/java/com/redhat/devtools/intellij/qute/psi/template/datamodel/AbstractInterfaceImplementationDataModelProvider.java b/src/main/java/com/redhat/devtools/intellij/qute/psi/template/datamodel/AbstractInterfaceImplementationDataModelProvider.java index 6357ef373..61c9d5805 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/psi/template/datamodel/AbstractInterfaceImplementationDataModelProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/psi/template/datamodel/AbstractInterfaceImplementationDataModelProvider.java @@ -1,85 +1,93 @@ /******************************************************************************* -* Copyright (c) 2024 Red Hat Inc. and others. -* All rights reserved. This program and the accompanying materials -* which accompanies this distribution, and is available at -* http://www.eclipse.org/legal/epl-v20.html -* -* SPDX-License-Identifier: EPL-2.0 -* -* Contributors: -* Red Hat Inc. - initial API and implementation -*******************************************************************************/ + * Copyright (c) 2024 Red Hat Inc. and others. + * All rights reserved. This program and the accompanying materials + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ package com.redhat.devtools.intellij.qute.psi.template.datamodel; -import java.util.logging.Level; -import java.util.logging.Logger; - +import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.IndexNotReadyException; import com.intellij.psi.PsiClass; import com.intellij.util.Query; import org.jetbrains.annotations.NotNull; +import java.util.concurrent.CancellationException; +import java.util.logging.Level; +import java.util.logging.Logger; + /** * Abstract class for data model provider based on class type search which * implements some interfaces. * * @author Angelo ZERR - * */ public abstract class AbstractInterfaceImplementationDataModelProvider extends AbstractDataModelProvider { - private static final Logger LOGGER = Logger - .getLogger(AbstractInterfaceImplementationDataModelProvider.class.getName()); + private static final Logger LOGGER = Logger + .getLogger(AbstractInterfaceImplementationDataModelProvider.class.getName()); - @Override - protected String[] getPatterns() { - return getInterfaceNames(); - } + @Override + protected String[] getPatterns() { + return getInterfaceNames(); + } - /** - * Returns the interface names to search. - * - * @return the interface names to search. - */ - protected abstract String[] getInterfaceNames(); + /** + * Returns the interface names to search. + * + * @return the interface names to search. + */ + protected abstract String[] getInterfaceNames(); - @Override - protected Query createSearchPattern(SearchContext context, String interfaceName) { - return createInterfaceImplementationSearchPattern(context, interfaceName); - } + @Override + protected Query createSearchPattern(SearchContext context, String interfaceName) { + return createInterfaceImplementationSearchPattern(context, interfaceName); + } - @Override - public void collectDataModel(Object match, SearchContext context, ProgressIndicator monitor) { - Object element = match; - if (element instanceof PsiClass type) { - try { - if (isApplicable(type)) { - processType(type, context, monitor); - } - } catch (Exception e) { - if (LOGGER.isLoggable(Level.SEVERE)) { - LOGGER.log(Level.SEVERE, - "Cannot collect Qute data model for the type '" + type.getQualifiedName() + "'.", e); - } - } - } - } + @Override + public void collectDataModel(Object match, SearchContext context, ProgressIndicator monitor) { + Object element = match; + if (element instanceof PsiClass type) { + try { + if (isApplicable(type)) { + processType(type, context, monitor); + } + } catch (ProcessCanceledException e) { + //Since 2024.2 ProcessCanceledException extends CancellationException so we can't use multicatch to keep backward compatibility + //TODO delete block when minimum required version is 2024.2 + throw e; + } catch (IndexNotReadyException | CancellationException e) { + throw e; + } catch (Exception e) { + if (LOGGER.isLoggable(Level.SEVERE)) { + LOGGER.log(Level.SEVERE, + "Cannot collect Qute data model for the type '" + type.getQualifiedName() + "'.", e); + } + } + } + } - private boolean isApplicable(PsiClass type) { - PsiClass @NotNull [] superInterfaceNames = type.getInterfaces(); - if (superInterfaceNames == null || superInterfaceNames.length == 0) { - return false; - } - for (String interfaceName : getInterfaceNames()) { - for (PsiClass superInterfaceName : superInterfaceNames) { - if (interfaceName.equals(superInterfaceName.getQualifiedName())) { - return true; - } - } - } - return false; - } + private boolean isApplicable(PsiClass type) { + PsiClass @NotNull [] superInterfaceNames = type.getInterfaces(); + if (superInterfaceNames == null || superInterfaceNames.length == 0) { + return false; + } + for (String interfaceName : getInterfaceNames()) { + for (PsiClass superInterfaceName : superInterfaceNames) { + if (interfaceName.equals(superInterfaceName.getQualifiedName())) { + return true; + } + } + } + return false; + } - protected abstract void processType(PsiClass recordElement, SearchContext context, ProgressIndicator monitor); + protected abstract void processType(PsiClass recordElement, SearchContext context, ProgressIndicator monitor); } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 825c697de..dc42177d9 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -376,6 +376,9 @@ + + +