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) {