Prevent race conditions during CLI install  (#2523)

* Use a special exitcode when connecting to CLI as client. Do not run upgrade step thereafter.

* Piggyback with NBJLS server relaunch after installation completes.

* Delay and/or disable the restart LSP client after connection termination
with the hope to finish install tasks meanwhile and then restart explicitly

* Wait after last child closes on Win.

* Fixed launchers release number -> release.

Co-authored-by: Jaroslav Tulach <jaroslav.tulach@oracle.com>
diff --git a/java/java.lsp.server/nbcode/nbproject/genfiles.properties b/java/java.lsp.server/nbcode/nbproject/genfiles.properties
index d7a1e40..48caec6 100644
--- a/java/java.lsp.server/nbcode/nbproject/genfiles.properties
+++ b/java/java.lsp.server/nbcode/nbproject/genfiles.properties
@@ -3,9 +3,9 @@
 build.xml.stylesheet.CRC32=70ce5c94@2.80
 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
 # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
-nbproject/build-impl.xml.data.CRC32=99f9616a
+nbproject/build-impl.xml.data.CRC32=48c0390b
 nbproject/build-impl.xml.script.CRC32=e03e5352
-nbproject/build-impl.xml.stylesheet.CRC32=473dc988@2.80
+nbproject/build-impl.xml.stylesheet.CRC32=473dc988@2.82
 nbproject/platform.xml.data.CRC32=99f9616a
 nbproject/platform.xml.script.CRC32=6dcbd131
 nbproject/platform.xml.stylesheet.CRC32=ae64f0b6@2.80
diff --git a/java/java.lsp.server/vscode/package.json b/java/java.lsp.server/vscode/package.json
index 5b51355..963da4d 100644
--- a/java/java.lsp.server/vscode/package.json
+++ b/java/java.lsp.server/vscode/package.json
@@ -46,9 +46,9 @@
 					"default": false,
 					"description": "Enables verbose messages from the Apache NetBeans Language Server"
 				},
-				"netbeans.conflict.check" : {
-					"type" : "boolean",
-					"default" : true,
+				"netbeans.conflict.check": {
+					"type": "boolean",
+					"default": true,
 					"description": "Avoid conflicts with other Java extensions"
 				},
 				"java.test.editor.enableShortcuts": {
diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts
index 7a3df78..2981f72 100644
--- a/java/java.lsp.server/vscode/src/extension.ts
+++ b/java/java.lsp.server/vscode/src/extension.ts
@@ -168,13 +168,73 @@
     }));
 }
 
+/**
+ * Pending maintenance (install) task, activations should be chained after it.
+ */
+let maintenance : Promise<void> | null;
+
+/**
+ * Pending activation flag. Will be cleared when the process produces some message or fails.
+ */
+let activationPending : boolean = false;
+
 function activateWithJDK(specifiedJDK: string | null, context: ExtensionContext, log : vscode.OutputChannel, notifyKill: boolean): void {
-    if (nbProcess) {
+    const a : Promise<void> | null = maintenance;
+    if (activationPending) {
+        // do not activate more than once in parallel.
+        console.log("Server activation requested repeatedly, ignoring...");
+        return;
+    }
+    activationPending = true;
+    // chain the restart after termination of the former process.
+    if (a != null) {
+        console.log("Server activation initiated while in maintenance mode, scheduling after maintenance");
+        a.then(() => killNbProcess(notifyKill, log)).
+            then(() => doActivateWithJDK(specifiedJDK, context, log, notifyKill));
+    } else {
+        console.log("Initiating server activation");
+        killNbProcess(notifyKill, log).then(
+            () => doActivateWithJDK(specifiedJDK, context, log, notifyKill)
+        );
+    }
+}
+
+function killNbProcess(notifyKill : boolean, log : vscode.OutputChannel, specProcess?: ChildProcess) : Promise<void> {
+    const p = nbProcess;
+    console.log("Request to kill LSP server.");
+    if (p && (!specProcess || specProcess == p)) {
         if (notifyKill) {
             vscode.window.setStatusBarMessage("Restarting Apache NetBeans Language Server.", 2000);
         }
-        nbProcess.kill();
+        return new Promise((resolve, reject) => {
+            nbProcess = null;
+            p.on('close', function(code: number) {
+                console.log("LSP server closed: " + p.pid)
+                resolve();
+            });
+            console.log("Killing LSP server " + p.pid);
+            if (!p.kill()) {
+                reject("Cannot kill");
+            }
+        });
+    } else {
+        let msg = "Cannot kill: ";
+        if (specProcess) {
+            msg += "Requested kill on " + specProcess.pid + ", ";
+        }
+        console.log(msg + "current process is " + (p ? p.pid : "None"));
+        return new Promise((res, rej) => { res(); });
     }
+}
+
+function doActivateWithJDK(specifiedJDK: string | null, context: ExtensionContext, log : vscode.OutputChannel, notifyKill: boolean): void {
+    maintenance = null;
+    let restartWithJDKLater : ((time: number, n: boolean) => void) = function restartLater(time: number, n : boolean) {
+        log.appendLine(`Restart of Apache Language Server requested in ${(time / 1000)} s.`);
+        setTimeout(() => {
+            activateWithJDK(specifiedJDK, context, log, n);
+        }, time);
+    };
 
     const beVerbose : boolean = workspace.getConfiguration('netbeans').get('verbose', false);
     let info = {
@@ -184,7 +244,6 @@
         jdkHome : specifiedJDK,
         verbose: beVerbose
     };
-
     let launchMsg = `Launching Apache NetBeans Language Server with ${specifiedJDK ? specifiedJDK : 'default system JDK'}`;
     log.appendLine(launchMsg);
     vscode.window.setStatusBarMessage(launchMsg, 2000);
@@ -192,6 +251,7 @@
     let ideRunning = new Promise((resolve, reject) => {
         let collectedText : string | null = '';
         function logAndWaitForEnabled(text: string) {
+            activationPending = false;
             log.append(text);
             if (collectedText == null) {
                 return;
@@ -203,6 +263,7 @@
             }
         }
         let p = launcher.launch(info, "--modules", "--list");
+        console.log("LSP server launching: " + p.pid);
         p.stdout.on('data', function(d: any) {
             logAndWaitForEnabled(d.toString());
         });
@@ -210,8 +271,8 @@
             logAndWaitForEnabled(d.toString());
         });
         nbProcess = p;
-        nbProcess.on('close', function(code: number) {
-            if (p == nbProcess && code != 0) {
+        p.on('close', function(code: number) {
+            if (p == nbProcess && code != 0 && code) {
                 vscode.window.showWarningMessage("Apache NetBeans Language Server exited with " + code);
             }
             if (collectedText != null) {
@@ -222,8 +283,10 @@
                     log.appendLine("Cannot find org.netbeans.modules.java.lsp.server in the log!");
                 }
                 log.show(false);
+                killNbProcess(false, log, p);
                 reject("Apache NetBeans Language Server not enabled!");
             } else {
+                console.log("LSP server " + p.pid + " terminated with " + code);
                 log.appendLine("Exit code " + code);
             }
         });
@@ -293,7 +356,8 @@
                     return ErrorAction.Continue;
                 },
                 closed : function(): CloseAction {
-                    activateWithJDK(specifiedJDK, context, log, false);
+                    log.appendLine("Connection to Apache NetBeans Language Server closed.");
+                    restartWithJDKLater(10000, false);
                     return CloseAction.DoNotRestart;
                 }
             }
@@ -314,6 +378,7 @@
             client.onNotification(StatusMessageRequest.type, showStatusBarMessage);
         });
     }).catch((reason) => {
+        activationPending = false;
         log.append(reason);
         window.showErrorMessage('Error initializing ' + reason);
     });
@@ -351,14 +416,32 @@
             const yes = "Install GPLv2+CPEx code";
             window.showErrorMessage("Additional Java Support is needed", yes).then(reply => {
                 if (yes === reply) {
-                    let installProcess = launcher.launch(info, "--modules", "--install", ".*nbjavac.*");
-                    let logData = function(d: any) {
-                        log.append(d.toString());
+                    vscode.window.setStatusBarMessage("Preparing Apache NetBeans Language Server for additional installation", 2000);
+                    restartWithJDKLater = function() {
+                        console.log("Ignoring request for restart of Apache NetBeans Language Server");
                     };
-                    installProcess.stdout.on('data', logData);
-                    installProcess.stderr.on('data', logData);
-                    installProcess.on('close', function(code: number) {
-                        log.append("Additional Java Support installed with exit code " + code);
+                    maintenance = new Promise((resolve, reject) => {
+                        const kill : Promise<void> = killNbProcess(false, log);
+                        kill.then(() => {
+                            let installProcess = launcher.launch(info, "-J-Dnetbeans.close=true", "--modules", "--install", ".*nbjavac.*");
+                            console.log("Launching installation process: " + installProcess.pid);
+                            let logData = function(d: any) {
+                                log.append(d.toString());
+                            };
+                            installProcess.stdout.on('data', logData);
+                            installProcess.stderr.on('data', logData);
+                            installProcess.addListener("error", reject);
+                            // MUST wait on 'close', since stdout is inherited by children. The installProcess dies but
+                            // the inherited stream will be closed by the last child dying.
+                            installProcess.on('close', function(code: number) {
+                                console.log("Installation completed: " + installProcess.pid);
+                                log.appendLine("Additional Java Support installed with exit code " + code);
+                                // will be actually run after maintenance is resolve()d.
+                                activateWithJDK(specifiedJDK, context, log, notifyKill)
+                                resolve();
+                            });
+                            return installProcess;
+                        });
                     });
                 }
             });
diff --git a/platform/o.n.bootstrap/external/binaries-list b/platform/o.n.bootstrap/external/binaries-list
index 511ab38..99132f1 100644
--- a/platform/o.n.bootstrap/external/binaries-list
+++ b/platform/o.n.bootstrap/external/binaries-list
@@ -14,4 +14,5 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-3289B87AB9345958E16F3285ED884F5C4DAB7C2D platform-launchers-10.0.zip
+0879C497EA45DF57BFF833A45AE9217FA447C713 platform-launchers-12.2.zip
+
diff --git a/platform/o.n.bootstrap/external/platform-launchers-10.0-license.txt b/platform/o.n.bootstrap/external/platform-launchers-12.2-license.txt
similarity index 99%
rename from platform/o.n.bootstrap/external/platform-launchers-10.0-license.txt
rename to platform/o.n.bootstrap/external/platform-launchers-12.2-license.txt
index b57feb7..c2ad62b 100644
--- a/platform/o.n.bootstrap/external/platform-launchers-10.0-license.txt
+++ b/platform/o.n.bootstrap/external/platform-launchers-12.2-license.txt
@@ -1,6 +1,6 @@
 Name: NetBeans Application Launchers
 Description: Windows Launchers for the NetBeans Platform
-Version: 10.0
+Version: 12.2
 License: Apache-2.0
 Source: https://netbeans.org/
 Origin: NetBeans
diff --git a/platform/o.n.bootstrap/launcher/unix/nbexec b/platform/o.n.bootstrap/launcher/unix/nbexec
old mode 100644
new mode 100755
index ca7d16b..f59096a
--- a/platform/o.n.bootstrap/launcher/unix/nbexec
+++ b/platform/o.n.bootstrap/launcher/unix/nbexec
@@ -433,6 +433,11 @@
     trap '' EXIT
     look_for_new_clusters
     # If we should update anything, do it and restart IDE.
+    if [ $exitcode -eq 4 ] ; then
+        # Just connected to CLI, not in charge of running Updater or whatever.
+        exitcode=0
+        break
+    fi
     run_updater=""
     look_for_post_runs "$plathome"
 
@@ -454,6 +459,5 @@
     fi
 
 done
-
 # and we exit.
 exit $exitcode
diff --git a/platform/o.n.bootstrap/launcher/windows/nbexec.cpp b/platform/o.n.bootstrap/launcher/windows/nbexec.cpp
index 2e63d02..47e490e 100644
--- a/platform/o.n.bootstrap/launcher/windows/nbexec.cpp
+++ b/platform/o.n.bootstrap/launcher/windows/nbexec.cpp
@@ -43,9 +43,16 @@
     return TRUE;
 }
 
+volatile int exitStatus = 0;
+
 void exitHook(int status) {
+    exitStatus = status;
     logMsg("Exit hook called with status %d", status);
-    launcher.onExit();
+    // do not handle possible restarts, if we are just CLI-connecting to a running process.
+    if (status != -252) {
+        launcher.onExit();
+    }
+    logMsg("Exit hook terminated.");
 }
 
 #define NBEXEC_EXPORT extern "C" __declspec(dllexport)
diff --git a/platform/o.n.bootstrap/launcher/windows/platformlauncher.cpp b/platform/o.n.bootstrap/launcher/windows/platformlauncher.cpp
index b29b309..8df954c 100644
--- a/platform/o.n.bootstrap/launcher/windows/platformlauncher.cpp
+++ b/platform/o.n.bootstrap/launcher/windows/platformlauncher.cpp
@@ -24,6 +24,8 @@
 #include "platformlauncher.h"
 #include "argnames.h"
 
+volatile extern int exitStatus;
+
 using namespace std;
 
 const char *PlatformLauncher::HELP_MSG =
@@ -662,6 +664,10 @@
 
 void PlatformLauncher::onExit() {
     logMsg("onExit()");
+    if (exitStatus == -252) {
+        logMsg("Exiting from CLI client, will not restart.");
+        return;
+    }
     
     if (exiting) {
         logMsg("Already exiting, no need to schedule restart");
@@ -714,7 +720,8 @@
         STARTUPINFO si = {0};
         PROCESS_INFORMATION pi = {0};
         si.cb = sizeof(STARTUPINFO);
-        if (!CreateProcess(NULL, cmdLineStr, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
+
+        if (!CreateProcess(NULL, cmdLineStr, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
             logErr(true, true, "Failed to create process.");
             return;
         }
diff --git a/platform/o.n.bootstrap/nbproject/project.properties b/platform/o.n.bootstrap/nbproject/project.properties
index e10ee5b..83ac777 100644
--- a/platform/o.n.bootstrap/nbproject/project.properties
+++ b/platform/o.n.bootstrap/nbproject/project.properties
@@ -20,10 +20,10 @@
 module.jar.dir=lib
 module.jar.basename=boot.jar
 release.launcher/unix/nbexec=lib/nbexec
-release.external/platform-launchers-10.0.zip!/nbexec.exe=lib/nbexec.exe
-release.external/platform-launchers-10.0.zip!/nbexec64.exe=lib/nbexec64.exe
-release.external/platform-launchers-10.0.zip!/nbexec.dll=lib/nbexec.dll
-release.external/platform-launchers-10.0.zip!/nbexec64.dll=lib/nbexec64.dll
+release.external/platform-launchers-12.2.zip!/nbexec.exe=lib/nbexec.exe
+release.external/platform-launchers-12.2.zip!/nbexec64.exe=lib/nbexec64.exe
+release.external/platform-launchers-12.2.zip!/nbexec.dll=lib/nbexec.dll
+release.external/platform-launchers-12.2.zip!/nbexec64.dll=lib/nbexec64.dll
 nbm.executable.files=lib/nbexec
 
 javadoc.arch=${basedir}/arch.xml
diff --git a/platform/o.n.bootstrap/src/org/netbeans/CLIHandler.java b/platform/o.n.bootstrap/src/org/netbeans/CLIHandler.java
index ebd7f2e..5e2bf4a 100644
--- a/platform/o.n.bootstrap/src/org/netbeans/CLIHandler.java
+++ b/platform/o.n.bootstrap/src/org/netbeans/CLIHandler.java
@@ -258,6 +258,7 @@
         public static final int CANNOT_CONNECT = -255;
         public static final int CANNOT_WRITE = -254;
         public static final int ALREADY_RUNNING = -253;
+        public static final int CONNECTED = -252;
         
         private final File lockFile;
         private final int port;
diff --git a/platform/o.n.bootstrap/src/org/netbeans/MainImpl.java b/platform/o.n.bootstrap/src/org/netbeans/MainImpl.java
index 577c392..b812f97 100644
--- a/platform/o.n.bootstrap/src/org/netbeans/MainImpl.java
+++ b/platform/o.n.bootstrap/src/org/netbeans/MainImpl.java
@@ -60,7 +60,7 @@
         int res = execute (args, System.in, System.out, System.err, m);
         if (res == -1) {
             // Connected to another running NB instance and succeeded in making a call.
-            return;
+            System.exit(CLIHandler.Status.CONNECTED);
         } else if (res != 0) {
             // Some CLIHandler refused the invocation
             if (res == Integer.MIN_VALUE) {