From 287ce416423004d883ba631a150bca8562c68e7e Mon Sep 17 00:00:00 2001
From: Pascal Neub
Date: Thu, 29 Sep 2022 11:08:40 +0200
Subject: [PATCH] feat: terminate NodeJS scripts on exit/close
---
pom.xml | 5 ++
.../nbm/nodejs/impl/NodeJSScriptExitHook.java | 74 +++++++++++++++++++
.../impl/runconfig/NodeJSScriptRunConfig.java | 43 ++++++++++-
3 files changed, 119 insertions(+), 3 deletions(-)
create mode 100644 src/main/java/de/adito/aditoweb/nbm/nodejs/impl/NodeJSScriptExitHook.java
diff --git a/pom.xml b/pom.xml
index f01b7e4..5d0874a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,11 @@
de-adito-metrics-api
${netbeans.version}-1.9.0
+
+ org.netbeans.modules
+ org-netbeans-core-output2
+ ${netbeans.version}-1.9.0
+
diff --git a/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/NodeJSScriptExitHook.java b/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/NodeJSScriptExitHook.java
new file mode 100644
index 0000000..0259e81
--- /dev/null
+++ b/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/NodeJSScriptExitHook.java
@@ -0,0 +1,74 @@
+package de.adito.aditoweb.nbm.nodejs.impl;
+
+import org.openide.*;
+import org.openide.modules.OnStop;
+import org.openide.util.NbBundle;
+
+import javax.swing.*;
+import java.util.Set;
+import java.util.concurrent.*;
+
+/**
+ * Hook that is executed when the designer closes.
+ * If NodeJS scripts are still running the user will be warned
+ * and has the option to terminate the scripts
+ *
+ * @author p.neub, 28.09.2022
+ */
+@OnStop
+public class NodeJSScriptExitHook implements Callable
+{
+ private static final Set> running = ConcurrentHashMap.newKeySet();
+
+ @NbBundle.Messages({
+ "LBL_ScriptExitConfirmTitle={0} NodeJS script(s) is/are still running...",
+ "LBL_ScriptExitConfirmMessage=Terminate {0} running NodeJS script(s)?",
+ "LBL_ScriptExitConfirmTerminateBtn=Terminate all",
+ "LBL_ScriptExitConfirmDetachBtn=Detach all",
+ "LBL_ScriptExitConfirmCancelBtn=Cancel exit",
+ })
+ @Override
+ public Boolean call()
+ {
+ int count = running.size();
+ if (count == 0)
+ return Boolean.TRUE;
+ NotifyDescriptor descriptor = new NotifyDescriptor.Confirmation(
+ Bundle.LBL_ScriptExitConfirmMessage(count),
+ Bundle.LBL_ScriptExitConfirmTitle(count));
+ descriptor.setOptions(new String[]{
+ Bundle.LBL_ScriptExitConfirmTerminateBtn(),
+ Bundle.LBL_ScriptExitConfirmDetachBtn(),
+ Bundle.LBL_ScriptExitConfirmCancelBtn(),
+ });
+ Object selected = DialogDisplayer.getDefault().notify(descriptor);
+ if (NotifyDescriptor.CLOSED_OPTION.equals(selected) || Bundle.LBL_ScriptExitConfirmCancelBtn().equals(selected))
+ return Boolean.FALSE;
+ if (Bundle.LBL_ScriptExitConfirmTerminateBtn().equals(selected))
+ running.forEach(f -> f.cancel(false));
+ return Boolean.TRUE;
+ }
+
+ /**
+ * Adds a running NodeJS process,
+ * so that it can be terminated cleanly when the Designer is closed
+ *
+ * @param pProcessFuture process future
+ */
+ public static void add(CompletableFuture pProcessFuture)
+ {
+ running.add(pProcessFuture);
+ }
+
+ /**
+ * Removes the NodeJS process so that the user will no longer be warned about the process
+ * this should be called when the process terminates, or the process is detached from the designer
+ *
+ * @param pProcessFuture process future
+ */
+ public static void remove(CompletableFuture pProcessFuture)
+ {
+ running.remove(pProcessFuture);
+ }
+
+}
diff --git a/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/runconfig/NodeJSScriptRunConfig.java b/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/runconfig/NodeJSScriptRunConfig.java
index bf1a2ba..f4a8e80 100644
--- a/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/runconfig/NodeJSScriptRunConfig.java
+++ b/src/main/java/de/adito/aditoweb/nbm/nodejs/impl/runconfig/NodeJSScriptRunConfig.java
@@ -1,6 +1,7 @@
package de.adito.aditoweb.nbm.nodejs.impl.runconfig;
import de.adito.aditoweb.nbm.nbide.nbaditointerface.javascript.node.*;
+import de.adito.aditoweb.nbm.nodejs.impl.NodeJSScriptExitHook;
import de.adito.aditoweb.nbm.nodejs.impl.actions.io.*;
import de.adito.nbm.runconfig.api.*;
import de.adito.nbm.runconfig.spi.IActiveConfigComponentProvider;
@@ -12,14 +13,18 @@
import org.jetbrains.annotations.NotNull;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.project.*;
+import org.netbeans.core.output2.adito.InputOutputExt;
+import org.openide.*;
+import org.openide.util.*;
import org.openide.windows.*;
import javax.swing.*;
+import java.beans.PropertyChangeListener;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.*;
-import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.*;
/**
* RunConfig to execute a single nodejs script
@@ -28,7 +33,6 @@
*/
class NodeJSScriptRunConfig implements IRunConfig
{
-
private final Project project;
private final INodeJSEnvironment environment;
private final String scriptName;
@@ -86,6 +90,12 @@ public void executeAsnyc(@NotNull ProgressHandle pProgressHandle)
}
}
+ @NbBundle.Messages({
+ "LBL_ScriptCloseTerminateTitle=NodeJS script is still running...",
+ "LBL_ScriptCloseTerminateMessage=Do you want to terminate the NodeJS script?",
+ "LBL_ScriptCloseTerminateTerminateBtn=Terminate",
+ "LBL_ScriptCloseTerminateDetachBtn=Detach",
+ })
private void run(@NotNull InputOutput pIo, @NotNull INodeJSExecutor pExecutor, @NotNull Subject>> pSubject)
{
try
@@ -107,7 +117,34 @@ private void run(@NotNull InputOutput pIo, @NotNull INodeJSExecutor pExecutor, @
String npmScript = Paths.get(environment.getPath().getParent(), "node_modules", "npm", "bin", "npm-cli.js").toString();
CompletableFuture future = pExecutor.executeAsync(environment, INodeJSExecBase.node(), out, err, null, npmScript, "run", scriptName);
pSubject.onNext(Optional.of(future));
- future.whenComplete((pExit, pEx) -> pSubject.onNext(Optional.of(future)));
+
+ PropertyChangeListener ioListener = evt -> {
+ // also remove, if the script is not stopped
+ NodeJSScriptExitHook.remove(future);
+
+ // no cancel option, because the closing of the output window can not be aborted here
+ NotifyDescriptor descriptor = new NotifyDescriptor.Confirmation(
+ Bundle.LBL_ScriptCloseTerminateMessage(),
+ Bundle.LBL_ScriptCloseTerminateTitle());
+ descriptor.setOptions(new String[]{
+ Bundle.LBL_ScriptCloseTerminateTerminateBtn(),
+ Bundle.LBL_ScriptCloseTerminateDetachBtn(),
+ });
+ Object selected = DialogDisplayer.getDefault().notify(descriptor);
+ if (Bundle.LBL_ScriptCloseTerminateTerminateBtn().equals(selected))
+ future.cancel(false);
+ };
+ if (pIo instanceof InputOutputExt)
+ ((InputOutputExt) pIo).addPropertyChangeListener(ioListener);
+
+ future.whenComplete((pExit, pEx) -> {
+ if (pIo instanceof InputOutputExt)
+ ((InputOutputExt) pIo).removePropertyChangeListener(ioListener);
+
+ NodeJSScriptExitHook.remove(future);
+ pSubject.onNext(Optional.of(future));
+ });
+ NodeJSScriptExitHook.add(future);
}
catch (IOException pEx)
{