diff --git a/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-runtime/src/main/java/org/apache/nifi/minifi/bootstrap/BootstrapRequestReader.java b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-runtime/src/main/java/org/apache/nifi/minifi/bootstrap/BootstrapRequestReader.java
index 89816d6c2853..b2cf4d6c186a 100644
--- a/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-runtime/src/main/java/org/apache/nifi/minifi/bootstrap/BootstrapRequestReader.java
+++ b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-runtime/src/main/java/org/apache/nifi/minifi/bootstrap/BootstrapRequestReader.java
@@ -22,7 +22,6 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
-import org.apache.nifi.util.LimitingInputStream;
public class BootstrapRequestReader {
private final String secretKey;
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/util/LimitingInputStream.java b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-runtime/src/main/java/org/apache/nifi/minifi/bootstrap/LimitingInputStream.java
similarity index 98%
rename from nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/util/LimitingInputStream.java
rename to minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-runtime/src/main/java/org/apache/nifi/minifi/bootstrap/LimitingInputStream.java
index ce3a6dbff066..d762d0d8d25f 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/util/LimitingInputStream.java
+++ b/minifi/minifi-nar-bundles/minifi-framework-bundle/minifi-framework/minifi-runtime/src/main/java/org/apache/nifi/minifi/bootstrap/LimitingInputStream.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.nifi.util;
+package org.apache.nifi.minifi.bootstrap;
import java.io.IOException;
import java.io.InputStream;
diff --git a/nifi-bootstrap/pom.xml b/nifi-bootstrap/pom.xml
index 2fee97fa2640..378962aa2e19 100644
--- a/nifi-bootstrap/pom.xml
+++ b/nifi-bootstrap/pom.xml
@@ -24,20 +24,17 @@ language governing permissions and limitations under the License. -->
org.slf4j
slf4j-api
-
- org.apache.nifi
- nifi-utils
- 2.0.0-SNAPSHOT
-
org.apache.nifi
nifi-security-cert-builder
2.0.0-SNAPSHOT
+
org.apache.nifi
nifi-properties-loader
2.0.0-SNAPSHOT
+ runtime
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/BootstrapCodec.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/BootstrapCodec.java
deleted file mode 100644
index 5d93a80a9593..000000000000
--- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/BootstrapCodec.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.bootstrap;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.util.Arrays;
-
-public class BootstrapCodec {
-
- private final RunNiFi runner;
- private final BufferedReader reader;
- private final BufferedWriter writer;
-
- public BootstrapCodec(final RunNiFi runner, final InputStream in, final OutputStream out) {
- this.runner = runner;
- this.reader = new BufferedReader(new InputStreamReader(in));
- this.writer = new BufferedWriter(new OutputStreamWriter(out));
- }
-
- public void communicate() throws IOException {
- final String line = reader.readLine();
- final String[] splits = line.split(" ");
- if (splits.length < 0) {
- throw new IOException("Received invalid command from NiFi: " + line);
- }
-
- final String cmd = splits[0];
- final String[] args;
- if (splits.length == 1) {
- args = new String[0];
- } else {
- args = Arrays.copyOfRange(splits, 1, splits.length);
- }
-
- try {
- processRequest(cmd, args);
- } catch (final InvalidCommandException ice) {
- throw new IOException("Received invalid command from NiFi: " + line + (ice.getMessage() == null ? "" : " - Details: " + ice.toString()));
- }
- }
-
- private void processRequest(final String cmd, final String[] args) throws InvalidCommandException, IOException {
- switch (cmd) {
- case "PORT": {
- if (args.length != 2) {
- throw new InvalidCommandException();
- }
-
- final int port;
- try {
- port = Integer.parseInt(args[0]);
- } catch (final NumberFormatException nfe) {
- throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535");
- }
-
- if (port < 1 || port > 65535) {
- throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535");
- }
-
- final String secretKey = args[1];
-
- runner.setNiFiCommandControlPort(port, secretKey);
- writer.write("OK");
- writer.newLine();
- writer.flush();
- }
- break;
- case "STARTED": {
- if (args.length != 1) {
- throw new InvalidCommandException("STARTED command must contain a status argument");
- }
-
- if (!"true".equals(args[0]) && !"false".equals(args[0])) {
- throw new InvalidCommandException("Invalid status for STARTED command; should be true or false, but was '" + args[0] + "'");
- }
-
- final boolean started = Boolean.parseBoolean(args[0]);
- runner.setNiFiStarted(started);
- writer.write("OK");
- writer.newLine();
- writer.flush();
- }
- break;
- }
- }
-}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/BootstrapProcess.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/BootstrapProcess.java
new file mode 100644
index 000000000000..57dd7ebb9f99
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/BootstrapProcess.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap;
+
+import org.apache.nifi.bootstrap.command.BootstrapCommand;
+import org.apache.nifi.bootstrap.command.BootstrapCommandProvider;
+import org.apache.nifi.bootstrap.command.CommandStatus;
+import org.apache.nifi.bootstrap.command.StandardBootstrapCommandProvider;
+import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Bootstrap Process responsible for reading configuration and maintaining application status
+ */
+public class BootstrapProcess {
+ /**
+ * Start Application
+ *
+ * @param arguments Array of arguments
+ */
+ public static void main(final String[] arguments) {
+ final BootstrapCommandProvider bootstrapCommandProvider = new StandardBootstrapCommandProvider();
+ final BootstrapCommand bootstrapCommand = bootstrapCommandProvider.getBootstrapCommand(arguments);
+ run(bootstrapCommand);
+ }
+
+ private static void run(final BootstrapCommand bootstrapCommand) {
+ bootstrapCommand.run();
+ final CommandStatus commandStatus = bootstrapCommand.getCommandStatus();
+ if (CommandStatus.RUNNING == commandStatus) {
+ final Logger logger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
+ logger.info("Bootstrap Process Running");
+ } else {
+ final int status = commandStatus.getStatus();
+ System.exit(status);
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/NiFiListener.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/NiFiListener.java
deleted file mode 100644
index 16fe1955cef8..000000000000
--- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/NiFiListener.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.bootstrap;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.InetSocketAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.nifi.bootstrap.util.LimitingInputStream;
-
-public class NiFiListener {
-
- private ServerSocket serverSocket;
- private volatile Listener listener;
-
- int start(final RunNiFi runner, final int listenPort) throws IOException {
- serverSocket = new ServerSocket();
- serverSocket.bind(new InetSocketAddress("localhost", listenPort));
-
- final int localPort = serverSocket.getLocalPort();
- listener = new Listener(serverSocket, runner);
- final Thread listenThread = new Thread(listener);
- listenThread.setName("Listen to NiFi");
- listenThread.setDaemon(true);
- listenThread.start();
- return localPort;
- }
-
- public void stop() throws IOException {
- final Listener listener = this.listener;
- if (listener == null) {
- return;
- }
-
- listener.stop();
- }
-
- private class Listener implements Runnable {
-
- private final ServerSocket serverSocket;
- private final ExecutorService executor;
- private final RunNiFi runner;
- private volatile boolean stopped = false;
-
- public Listener(final ServerSocket serverSocket, final RunNiFi runner) {
- this.serverSocket = serverSocket;
- this.executor = Executors.newFixedThreadPool(2, new ThreadFactory() {
- @Override
- public Thread newThread(final Runnable runnable) {
- final Thread t = Executors.defaultThreadFactory().newThread(runnable);
- t.setDaemon(true);
- t.setName("NiFi Bootstrap Command Listener");
- return t;
- }
- });
-
- this.runner = runner;
- }
-
- public void stop() throws IOException {
- stopped = true;
-
- executor.shutdown();
- try {
- executor.awaitTermination(3, TimeUnit.SECONDS);
- } catch (final InterruptedException ie) {
- }
-
- serverSocket.close();
- }
-
- @Override
- public void run() {
- while (!serverSocket.isClosed()) {
- try {
- if (stopped) {
- return;
- }
-
- final Socket socket;
- try {
- socket = serverSocket.accept();
- } catch (final IOException ioe) {
- if (stopped) {
- return;
- }
-
- throw ioe;
- }
-
- executor.submit(new Runnable() {
- @Override
- public void run() {
- try {
- // we want to ensure that we don't try to read data from an InputStream directly
- // by a BufferedReader because any user on the system could open a socket and send
- // a multi-gigabyte file without any new lines in order to crash the Bootstrap,
- // which in turn may cause the Shutdown Hook to shutdown NiFi.
- // So we will limit the amount of data to read to 4 KB
- final InputStream limitingIn = new LimitingInputStream(socket.getInputStream(), 4096);
- final BootstrapCodec codec = new BootstrapCodec(runner, limitingIn, socket.getOutputStream());
- codec.communicate();
- } catch (final Throwable t) {
- System.out.println("Failed to communicate with NiFi due to " + t);
- t.printStackTrace();
- } finally {
- try {
- socket.close();
- } catch (final IOException ioe) {
- }
- }
- }
- });
- } catch (final Throwable t) {
- System.err.println("Failed to receive information from NiFi due to " + t);
- t.printStackTrace();
- }
- }
- }
- }
-}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
deleted file mode 100644
index 89d0693e1082..000000000000
--- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/RunNiFi.java
+++ /dev/null
@@ -1,1500 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.bootstrap;
-
-import org.apache.nifi.bootstrap.process.RuntimeValidatorExecutor;
-import org.apache.nifi.bootstrap.property.ApplicationPropertyHandler;
-import org.apache.nifi.bootstrap.property.SecurityApplicationPropertyHandler;
-import org.apache.nifi.bootstrap.util.DumpFileValidator;
-import org.apache.nifi.util.file.FileUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.lang.management.ManagementFactory;
-import java.lang.reflect.Method;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.InvalidPathException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Properties;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-import javax.management.MBeanServer;
-import javax.management.ObjectName;
-
-/**
- *
- * The class which bootstraps Apache NiFi. This class looks for the
- * bootstrap.conf file by looking in the following places (in order):
- *
- * Java System Property named
- * {@code org.apache.nifi.bootstrap.config.file}
- * ${NIFI_HOME}/./conf/bootstrap.conf, where ${NIFI_HOME} references an
- * environment variable {@code NIFI_HOME}
- * ./conf/bootstrap.conf, where {@code ./} represents the working
- * directory.
- *
- *
- * If the {@code bootstrap.conf} file cannot be found, throws a {@code FileNotFoundException}.
- */
-public class RunNiFi {
-
- public static final String DEFAULT_CONFIG_FILE = "./conf/bootstrap.conf";
- public static final String DEFAULT_JAVA_CMD = "java";
- public static final String DEFAULT_PID_DIR = "bin";
- public static final String DEFAULT_LOG_DIR = "./logs";
- public static final String DEFAULT_STATUS_HISTORY_DAYS = "1";
-
- public static final String GRACEFUL_SHUTDOWN_PROP = "graceful.shutdown.seconds";
- public static final String DEFAULT_GRACEFUL_SHUTDOWN_VALUE = "20";
-
- public static final String NIFI_PID_DIR_PROP = "org.apache.nifi.bootstrap.config.pid.dir";
-
- public static final String NIFI_PID_FILE_NAME = "nifi.pid";
- public static final String NIFI_STATUS_FILE_NAME = "nifi.status";
- public static final String NIFI_LOCK_FILE_NAME = "nifi.lock";
-
- public static final String NIFI_BOOTSTRAP_LISTEN_PORT_PROP = "nifi.bootstrap.listen.port";
-
- public static final String PID_KEY = "pid";
-
- public static final int STARTUP_WAIT_SECONDS = 60;
- public static final long GRACEFUL_SHUTDOWN_RETRY_MILLIS = 2000L;
-
- public static final String SHUTDOWN_CMD = "SHUTDOWN";
- public static final String DECOMMISSION_CMD = "DECOMMISSION";
- public static final String PING_CMD = "PING";
- public static final String DUMP_CMD = "DUMP";
- public static final String DIAGNOSTICS_CMD = "DIAGNOSTICS";
- public static final String CLUSTER_STATUS_CMD = "CLUSTER_STATUS";
- public static final String IS_LOADED_CMD = "IS_LOADED";
- public static final String STATUS_HISTORY_CMD = "STATUS_HISTORY";
-
- private static final int UNINITIALIZED_CC_PORT = -1;
-
- private static final int INVALID_CMD_ARGUMENT = -1;
-
- private volatile boolean autoRestartNiFi = true;
- private volatile int ccPort = UNINITIALIZED_CC_PORT;
- private volatile long nifiPid = -1L;
- private volatile String secretKey;
- private volatile ShutdownHook shutdownHook;
- private volatile boolean nifiStarted;
-
- private final Lock startedLock = new ReentrantLock();
- private final Lock lock = new ReentrantLock();
- private final Condition startupCondition = lock.newCondition();
-
- private final File bootstrapConfigFile;
-
- // used for logging initial info; these will be logged to console by default when the app is started
- private final Logger cmdLogger = LoggerFactory.getLogger("org.apache.nifi.bootstrap.Command");
- // used for logging all info. These by default will be written to the log file
- private final Logger defaultLogger = LoggerFactory.getLogger(RunNiFi.class);
-
- private final ExecutorService loggingExecutor;
- private final RuntimeValidatorExecutor runtimeValidatorExecutor;
- private volatile Set> loggingFutures = new HashSet<>(2);
-
- public RunNiFi(final File bootstrapConfigFile) throws IOException {
- this.bootstrapConfigFile = bootstrapConfigFile;
-
- loggingExecutor = Executors.newFixedThreadPool(2, runnable -> {
- final Thread t = Executors.defaultThreadFactory().newThread(runnable);
- t.setDaemon(true);
- t.setName("NiFi logging handler");
- return t;
- });
-
- runtimeValidatorExecutor = new RuntimeValidatorExecutor();
- }
-
- private static void printUsage() {
- System.out.println("Usage:");
- System.out.println();
- System.out.println("java org.apache.nifi.bootstrap.RunNiFi [options]");
- System.out.println();
- System.out.println("Valid commands include:");
- System.out.println();
- System.out.println("Start : Start a new instance of Apache NiFi");
- System.out.println("Stop : Stop a running instance of Apache NiFi");
- System.out.println("Restart : Stop Apache NiFi, if it is running, and then start a new instance");
- System.out.println("Decommission : Disconnects Apache NiFi from its cluster, offloads its data to other nodes in the cluster, removes itself from the cluster, and shuts down the instance");
- System.out.println("Status : Determine if there is a running instance of Apache NiFi");
- System.out.println("Dump : Write a Thread Dump to the file specified by [options], or to the log if no file is given");
- System.out.println("Diagnostics : Write diagnostic information to the file specified by [options], or to the log if no file is given. The --verbose flag may be provided as an option before " +
- "the filename, which may result in additional diagnostic information being written.");
- System.out.println("Status-history : Save the status history to the file specified by [options]. The expected command parameters are: " +
- "status-history . The parameter is optional and defaults to 1 day.");
- System.out.println("Run : Start a new instance of Apache NiFi and monitor the Process, restarting if the instance dies");
- System.out.println();
- }
-
- public static void main(String[] args) throws IOException {
- if (args.length < 1 || args.length > 3) {
- printUsage();
- return;
- }
-
- File dumpFile = null;
- boolean verbose = false;
- String statusHistoryDays = null;
-
- final String cmd = args[0];
- if (cmd.equalsIgnoreCase("dump")) {
- if (args.length > 1) {
- dumpFile = new File(args[1]);
- }
- } else if (cmd.equalsIgnoreCase("diagnostics")) {
- if (args.length > 2) {
- verbose = args[1].equalsIgnoreCase("--verbose");
- dumpFile = new File(args[2]);
- } else if (args.length > 1) {
- if (args[1].equalsIgnoreCase("--verbose")) {
- verbose = true;
- } else {
- dumpFile = new File(args[1]);
- }
- }
- } else if (cmd.equalsIgnoreCase("cluster-status")) {
- if (args.length > 1) {
- dumpFile = new File(args[1]);
- }
- } else if (cmd.equalsIgnoreCase("status-history")) {
- if (args.length < 2) {
- System.err.printf("Wrong number of arguments: %d instead of 1 or 2, the command parameters are: " +
- "status-history %n", 0);
- System.exit(INVALID_CMD_ARGUMENT);
- }
- if (args.length == 3) {
- statusHistoryDays = args[1];
- try {
- final int numberOfDays = Integer.parseInt(statusHistoryDays);
- if (numberOfDays < 1) {
- System.err.println("The parameter must be positive and greater than zero. The command parameters are:" +
- " status-history ");
- System.exit(INVALID_CMD_ARGUMENT);
- }
- } catch (NumberFormatException e) {
- System.err.println("The parameter value is not a number. The command parameters are: status-history ");
- System.exit(INVALID_CMD_ARGUMENT);
- }
- try {
- Paths.get(args[2]);
- } catch (InvalidPathException e) {
- System.err.println("Invalid filename. The command parameters are: status-history ");
- System.exit(INVALID_CMD_ARGUMENT);
- }
- dumpFile = new File(args[2]);
- } else {
- final boolean isValid = DumpFileValidator.validate(args[1]);
- if (isValid) {
- statusHistoryDays = DEFAULT_STATUS_HISTORY_DAYS;
- dumpFile = new File(args[1]);
- } else {
- System.exit(INVALID_CMD_ARGUMENT);
- }
- }
- }
-
- switch (cmd.toLowerCase()) {
- case "start":
- case "run":
- case "stop":
- case "decommission":
- case "status":
- case "is_loaded":
- case "dump":
- case "diagnostics":
- case "status-history":
- case "restart":
- case "env":
- case "cluster-status":
- break;
- default:
- printUsage();
- return;
- }
-
- final File configFile = getDefaultBootstrapConfFile();
- final RunNiFi runNiFi = new RunNiFi(configFile);
-
- Integer exitStatus = null;
- switch (cmd.toLowerCase()) {
- case "start":
- runNiFi.start(true);
- break;
- case "run":
- runNiFi.start(true);
- break;
- case "stop":
- runNiFi.stop();
- break;
- case "decommission":
- final boolean shutdown = args.length < 2 || !"--shutdown=false".equals(args[1]);
- exitStatus = runNiFi.decommission(shutdown);
- break;
- case "status":
- exitStatus = runNiFi.status();
- break;
- case "is_loaded":
- try {
- System.out.println(runNiFi.isNiFiFullyLoaded());
- } catch (NiFiNotRunningException e) {
- System.out.println("not_running");
- }
- break;
- case "restart":
- runNiFi.stop();
- runNiFi.start(true);
- break;
- case "dump":
- runNiFi.dump(dumpFile);
- break;
- case "diagnostics":
- runNiFi.diagnostics(dumpFile, verbose);
- break;
- case "cluster-status":
- runNiFi.clusterStatus(dumpFile);
- break;
- case "status-history":
- runNiFi.statusHistory(dumpFile, statusHistoryDays);
- break;
- case "env":
- runNiFi.env();
- break;
- }
- if (exitStatus != null) {
- System.exit(exitStatus);
- }
- }
-
- private static File getDefaultBootstrapConfFile() {
- String configFilename = System.getProperty("org.apache.nifi.bootstrap.config.file");
-
- if (configFilename == null) {
- final String nifiHome = System.getenv("NIFI_HOME");
- if (nifiHome != null) {
- final File nifiHomeFile = new File(nifiHome.trim());
- final File configFile = new File(nifiHomeFile, DEFAULT_CONFIG_FILE);
- configFilename = configFile.getAbsolutePath();
- }
- }
-
- if (configFilename == null) {
- configFilename = DEFAULT_CONFIG_FILE;
- }
-
- return new File(configFilename);
- }
-
- protected File getBootstrapFile(final Logger logger, String directory, String defaultDirectory, String fileName) throws IOException {
-
- final File confDir = bootstrapConfigFile.getParentFile();
- final File nifiHome = confDir.getParentFile();
-
- String confFileDir = System.getProperty(directory);
-
- final File fileDir;
-
- if (confFileDir != null) {
- fileDir = new File(confFileDir.trim());
- } else {
- fileDir = new File(nifiHome, defaultDirectory);
- }
-
- FileUtils.ensureDirectoryExistAndCanAccess(fileDir);
- final File statusFile = new File(fileDir, fileName);
- logger.debug("Status File: {}", statusFile);
- return statusFile;
- }
-
- protected File getPidFile(final Logger logger) throws IOException {
- return getBootstrapFile(logger, NIFI_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_PID_FILE_NAME);
- }
-
- protected File getStatusFile(final Logger logger) throws IOException {
- return getBootstrapFile(logger, NIFI_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_STATUS_FILE_NAME);
- }
-
- protected File getLockFile(final Logger logger) throws IOException {
- return getBootstrapFile(logger, NIFI_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_LOCK_FILE_NAME);
- }
-
- protected File getStatusFile() throws IOException {
- return getStatusFile(defaultLogger);
- }
-
- private Properties loadProperties(final Logger logger) throws IOException {
- final Properties props = new Properties();
- final File statusFile = getStatusFile(logger);
- if (statusFile == null || !statusFile.exists()) {
- logger.debug("No status file to load properties from");
- return props;
- }
-
- try (final FileInputStream fis = new FileInputStream(getStatusFile(logger))) {
- props.load(fis);
- }
-
- final Map modified = new HashMap<>(props);
- modified.remove("secret.key");
- logger.debug("Properties: {}", modified);
-
- return props;
- }
-
- private synchronized void savePidProperties(final Properties pidProperties, final Logger logger) throws IOException {
- final String pid = pidProperties.getProperty(PID_KEY);
- if (pid != null && !pid.isBlank()) {
- writePidFile(pid, logger);
- }
-
- final File statusFile = getStatusFile(logger);
- if (statusFile.exists() && !statusFile.delete()) {
- logger.warn("Failed to delete {}", statusFile);
- }
-
- if (!statusFile.createNewFile()) {
- throw new IOException("Failed to create file " + statusFile);
- }
-
- try {
- final Set perms = new HashSet<>();
- perms.add(PosixFilePermission.OWNER_READ);
- perms.add(PosixFilePermission.OWNER_WRITE);
- Files.setPosixFilePermissions(statusFile.toPath(), perms);
- } catch (final Exception e) {
- logger.warn("Failed to set permissions so that only the owner can read status file {}; "
- + "this may allows others to have access to the key needed to communicate with NiFi. "
- + "Permissions should be changed so that only the owner can read this file", statusFile);
- }
-
- try (final FileOutputStream fos = new FileOutputStream(statusFile)) {
- pidProperties.store(fos, null);
- fos.getFD().sync();
- }
-
- logger.debug("Saved Properties {} to {}", pidProperties, statusFile);
- }
-
- private synchronized void writePidFile(final String pid, final Logger logger) throws IOException {
- final File pidFile = getPidFile(logger);
- if (pidFile.exists() && !pidFile.delete()) {
- logger.warn("Failed to delete {}", pidFile);
- }
-
- if (!pidFile.createNewFile()) {
- throw new IOException("Failed to create file " + pidFile);
- }
-
- try {
- final Set perms = new HashSet<>();
- perms.add(PosixFilePermission.OWNER_WRITE);
- perms.add(PosixFilePermission.OWNER_READ);
- perms.add(PosixFilePermission.GROUP_READ);
- perms.add(PosixFilePermission.OTHERS_READ);
- Files.setPosixFilePermissions(pidFile.toPath(), perms);
- } catch (final Exception e) {
- logger.warn("Failed to set permissions so that only the owner can read pid file {}; "
- + "this may allows others to have access to the key needed to communicate with NiFi. "
- + "Permissions should be changed so that only the owner can read this file", pidFile);
- }
-
- try (final FileOutputStream fos = new FileOutputStream(pidFile)) {
- fos.write(pid.getBytes(StandardCharsets.UTF_8));
- fos.getFD().sync();
- }
-
- logger.debug("Saved PID [{}] to [{}]", pid, pidFile);
- }
-
- private boolean isPingSuccessful(final int port, final String secretKey, final Logger logger) {
- logger.debug("Pinging {}", port);
-
- try (final Socket socket = new Socket("localhost", port)) {
- final OutputStream out = socket.getOutputStream();
- out.write((PING_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
- out.flush();
-
- logger.debug("Sent PING command");
- socket.setSoTimeout(5000);
- final InputStream in = socket.getInputStream();
- final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
- final String response = reader.readLine();
- logger.debug("PING response: {}", response);
- out.close();
- reader.close();
-
- return PING_CMD.equals(response);
- } catch (final IOException ioe) {
- return false;
- }
- }
-
- private Integer getCurrentPort(final Logger logger) throws IOException {
- final Properties props = loadProperties(logger);
- final String portVal = props.getProperty("port");
- if (portVal == null) {
- logger.debug("No Port found in status file");
- return null;
- } else {
- logger.debug("Port defined in status file: {}", portVal);
- }
-
- final int port = Integer.parseInt(portVal);
- final boolean success = isPingSuccessful(port, props.getProperty("secret.key"), logger);
- if (success) {
- logger.debug("Successful PING on port {}", port);
- return port;
- }
-
- final String pid = props.getProperty(PID_KEY);
- logger.debug("PID in status file is {}", pid);
- if (pid != null) {
- final boolean procRunning = isProcessRunning(pid, logger);
- if (procRunning) {
- return port;
- } else {
- return null;
- }
- }
-
- return null;
- }
-
- private boolean isProcessRunning(final String pid, final Logger logger) {
- try {
- // We use the "ps" command to check if the process is still running.
- final ProcessBuilder builder = new ProcessBuilder();
-
- builder.command("ps", "-p", pid);
- final Process proc = builder.start();
-
- // Look for the pid in the output of the 'ps' command.
- boolean running = false;
- String line;
- try (final InputStream in = proc.getInputStream();
- final Reader streamReader = new InputStreamReader(in);
- final BufferedReader reader = new BufferedReader(streamReader)) {
-
- while ((line = reader.readLine()) != null) {
- if (line.trim().startsWith(pid)) {
- running = true;
- }
- }
- }
-
- // If output of the ps command had our PID, the process is running.
- if (running) {
- logger.debug("Process with PID {} is running", pid);
- } else {
- logger.debug("Process with PID {} is not running", pid);
- }
-
- return running;
- } catch (final IOException ioe) {
- System.err.println("Failed to determine if Process " + pid + " is running; assuming that it is not");
- return false;
- }
- }
-
- private Status getStatus(final Logger logger) {
- final Properties props;
- try {
- props = loadProperties(logger);
- } catch (final IOException ioe) {
- return new Status(null, null, false, false);
- }
-
- if (props == null) {
- return new Status(null, null, false, false);
- }
-
- final String portValue = props.getProperty("port");
- final String pid = props.getProperty(PID_KEY);
- final String secretKey = props.getProperty("secret.key");
-
- if (portValue == null && pid == null) {
- return new Status(null, null, false, false);
- }
-
- Integer port = null;
- boolean pingSuccess = false;
- if (portValue != null) {
- try {
- port = Integer.parseInt(portValue);
- pingSuccess = isPingSuccessful(port, secretKey, logger);
- } catch (final NumberFormatException nfe) {
- return new Status(null, null, false, false);
- }
- }
-
- if (pingSuccess) {
- return new Status(port, pid, true, true);
- }
-
- final boolean alive = pid != null && isProcessRunning(pid, logger);
- return new Status(port, pid, pingSuccess, alive);
- }
-
- public int status() throws IOException {
- final Logger logger = cmdLogger;
- final Status status = getStatus(logger);
- if (status.isRespondingToPing()) {
- logger.info("Apache NiFi PID [{}] running with Bootstrap Port [{}]", status.getPid(), status.getPort());
- return 0;
- }
-
- if (status.isProcessRunning()) {
- logger.info("Apache NiFi PID [{}] running but not responding with Bootstrap Port [{}]", status.getPid(), status.getPort());
- return 4;
- }
-
- if (status.getPort() == null) {
- logger.info("Apache NiFi is not running");
- return 3;
- }
-
- if (status.getPid() == null) {
- logger.info("Apache NiFi is not responding to Ping requests. The process may have died or may be hung");
- } else {
- logger.info("Apache NiFi is not running");
- }
- return 3;
- }
-
- public void env() {
- final Logger logger = cmdLogger;
- final Status status = getStatus(logger);
- if (status.getPid() == null) {
- logger.info("Apache NiFi is not running");
- return;
- }
- final Class> virtualMachineClass;
- try {
- virtualMachineClass = Class.forName("com.sun.tools.attach.VirtualMachine");
- } catch (final ClassNotFoundException cnfe) {
- logger.error("Seems tools.jar (Linux / Windows JDK) or classes.jar (Mac OS) is not available in classpath");
- return;
- }
- final Method attachMethod;
- final Method detachMethod;
-
- try {
- attachMethod = virtualMachineClass.getMethod("attach", String.class);
- detachMethod = virtualMachineClass.getDeclaredMethod("detach");
- } catch (final Exception e) {
- logger.error("Methods required for getting environment not available", e);
- return;
- }
-
- final Object virtualMachine;
- try {
- virtualMachine = attachMethod.invoke(null, status.getPid());
- } catch (final Throwable t) {
- logger.error("Problem attaching to NiFi", t);
- return;
- }
-
- try {
- final Method getSystemPropertiesMethod = virtualMachine.getClass().getMethod("getSystemProperties");
-
- final Properties sysProps = (Properties) getSystemPropertiesMethod.invoke(virtualMachine);
- for (Entry syspropEntry : sysProps.entrySet()) {
- logger.info("{} = {}", syspropEntry.getKey(), syspropEntry.getValue());
- }
- } catch (Throwable t) {
- throw new RuntimeException(t);
- } finally {
- try {
- detachMethod.invoke(virtualMachine);
- } catch (final Exception e) {
- logger.warn("Caught exception detaching from process", e);
- }
- }
- }
-
- /**
- * Writes NiFi diagnostic information to the given file; if the file is null, logs at INFO level instead.
- */
- public void diagnostics(final File dumpFile, final boolean verbose) throws IOException {
- final String args = verbose ? "--verbose=true" : null;
- makeRequest(DIAGNOSTICS_CMD, args, dumpFile, null, "diagnostics information");
- }
-
- public void clusterStatus(final File dumpFile) throws IOException {
- try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
- makeRequest(CLUSTER_STATUS_CMD, null, dumpFile, baos, "cluster status");
-
- final String response = baos.toString(StandardCharsets.UTF_8);
- System.out.println("Cluster Status: " + response);
- }
- }
-
- /**
- * Writes a NiFi thread dump to the given file; if file is null, logs at
- * INFO level instead.
- *
- * @param dumpFile the file to write the dump content to
- * @throws IOException if any issues occur while writing the dump file
- */
- public void dump(final File dumpFile) throws IOException {
- makeRequest(DUMP_CMD, null, dumpFile, null, "thread dump");
- }
-
- /**
- * Writes NiFi status history information to the given file.
- *
- * @param dumpFile the file to write the dump content to
- * @throws IOException if any issues occur while writing the dump file
- */
- public void statusHistory(final File dumpFile, final String days) throws IOException {
- // Due to input validation, the dumpFile cannot currently be null in this scenario.
- makeRequest(STATUS_HISTORY_CMD, days, dumpFile, null, "status history information");
- }
-
- private boolean isNiFiFullyLoaded() throws IOException, NiFiNotRunningException {
- final Logger logger = defaultLogger;
- final Integer port = getCurrentPort(logger);
- if (port == null) {
- logger.info("Apache NiFi is not currently running");
- throw new NiFiNotRunningException();
- }
-
- try (final Socket socket = new Socket()) {
- sendRequest(socket, port, IS_LOADED_CMD, null, logger);
-
- final InputStream in = socket.getInputStream();
- try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
- String line = reader.readLine();
- return Boolean.parseBoolean(line);
- }
- }
- }
-
- /**
- * Makes a request to the Bootstrap Listener
- * @param request the request to send
- * @param arguments any arguments for the command, or null
if the command takes no arguments
- * @param dumpFile a file to write the results to, or null
to skip writing the results to any file
- * @param outputStream an OutputStream to write the results to, or null
to skip writing the results to any OutputStream
- * @param contentsDescription a description of the contents being written; used for logging purposes
- * @throws IOException if unable to communicate with the NiFi instance or write out the results
- */
- private void makeRequest(final String request, final String arguments, final File dumpFile, final OutputStream outputStream, final String contentsDescription) throws IOException {
- final Logger logger = defaultLogger; // dump to bootstrap log file by default
- final Integer port = getCurrentPort(logger);
- if (port == null) {
- cmdLogger.info("Apache NiFi is not currently running");
- logger.info("Apache NiFi is not currently running");
- return;
- }
-
- final OutputStream fileOut = dumpFile == null ? null : new FileOutputStream(dumpFile);
- try {
- try (final Socket socket = new Socket()) {
- sendRequest(socket, port, request, arguments, logger);
-
- final InputStream in = socket.getInputStream();
- try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
- String line;
- while ((line = reader.readLine()) != null) {
- boolean written = false;
- if (fileOut != null) {
- fileOut.write(line.getBytes(StandardCharsets.UTF_8));
- fileOut.write('\n');
- written = true;
- }
-
- if (outputStream != null) {
- outputStream.write(line.getBytes(StandardCharsets.UTF_8));
- outputStream.write('\n');
- written = true;
- }
-
- if (!written) {
- logger.info(line);
- }
- }
- }
- }
- } finally {
- if (fileOut != null) {
- fileOut.close();
- cmdLogger.info("Successfully wrote {} to {}", contentsDescription, dumpFile.getAbsolutePath());
- }
- }
- }
-
- private void sendRequest(Socket socket, Integer port, String request, String arguments, Logger logger) throws IOException {
- logger.debug("Connecting to NiFi instance");
- socket.setSoTimeout(60000);
- socket.connect(new InetSocketAddress("localhost", port));
- logger.debug("Established connection to NiFi instance.");
- socket.setSoTimeout(60000);
-
- logger.debug("Sending {} Command to port {}", request, port);
- final OutputStream socketOut = socket.getOutputStream();
-
- final Properties nifiProps = loadProperties(logger);
- final String secretKey = nifiProps.getProperty("secret.key");
-
- if (arguments == null) {
- socketOut.write((request + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
- } else {
- socketOut.write((request + " " + secretKey + " " + arguments + "\n").getBytes(StandardCharsets.UTF_8));
- }
-
- socketOut.flush();
- }
-
-
- public Integer decommission(final boolean shutdown) throws IOException {
- final Logger logger = cmdLogger;
- final Integer port = getCurrentPort(logger);
- if (port == null) {
- logger.info("Apache NiFi is not currently running");
- return 15;
- }
-
- // indicate that a stop command is in progress
- final File lockFile = getLockFile(logger);
- if (!lockFile.exists()) {
- lockFile.createNewFile();
- }
-
- final Properties nifiProps = loadProperties(logger);
- final String secretKey = nifiProps.getProperty("secret.key");
- final String pid = nifiProps.getProperty(PID_KEY);
- final File statusFile = getStatusFile(logger);
- final File pidFile = getPidFile(logger);
-
- try (final Socket socket = new Socket()) {
- logger.debug("Connecting to NiFi instance");
- socket.setSoTimeout(10000);
- socket.connect(new InetSocketAddress("localhost", port));
- logger.debug("Established connection to NiFi instance.");
-
- // We don't know how long it will take for the offloading to complete. It could be a while. So don't timeout.
- // User can press Ctrl+C to terminate if they don't want to wait
- socket.setSoTimeout(0);
-
- logger.debug("Sending DECOMMISSION Command to port {}", port);
- final OutputStream out = socket.getOutputStream();
- final String command = DECOMMISSION_CMD + " " + secretKey + " --shutdown=" + shutdown + "\n";
- out.write(command.getBytes(StandardCharsets.UTF_8));
- out.flush();
- socket.shutdownOutput();
-
- final String response = readResponse(socket.getInputStream());
-
- if (DECOMMISSION_CMD.equals(response)) {
- logger.debug("Received response to DECOMMISSION command: {}", response);
-
- if (pid != null) {
- waitForShutdown(pid, logger, statusFile, pidFile);
- }
-
- return null;
- } else {
- logger.error("When sending DECOMMISSION command to NiFi, got unexpected response {}", response);
- return 18;
- }
- } finally {
- if (lockFile.exists() && !lockFile.delete()) {
- logger.error("Failed to delete lock file {}; this file should be cleaned up manually", lockFile);
- }
- }
- }
-
- public void stop() throws IOException {
- final Logger logger = cmdLogger;
- final Integer port = getCurrentPort(logger);
- if (port == null) {
- logger.info("Apache NiFi is not currently running");
- return;
- }
-
- // indicate that a stop command is in progress
- final File lockFile = getLockFile(logger);
- if (!lockFile.exists()) {
- lockFile.createNewFile();
- }
-
- final Properties nifiProps = loadProperties(logger);
- final String secretKey = nifiProps.getProperty("secret.key");
- final String pid = nifiProps.getProperty(PID_KEY);
- final File statusFile = getStatusFile(logger);
- final File pidFile = getPidFile(logger);
-
- try (final Socket socket = new Socket()) {
- logger.debug("Connecting to NiFi instance");
- socket.setSoTimeout(10000);
- socket.connect(new InetSocketAddress("localhost", port));
- logger.debug("Established connection to NiFi instance.");
- socket.setSoTimeout(10000);
-
- logger.debug("Sending SHUTDOWN Command to port {}", port);
- final OutputStream out = socket.getOutputStream();
- out.write((SHUTDOWN_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
- out.flush();
- socket.shutdownOutput();
-
- final String response = readResponse(socket.getInputStream());
- logger.debug("Received response to SHUTDOWN command: {}", response);
-
- if (SHUTDOWN_CMD.equals(response)) {
- logger.info("Apache NiFi has accepted the Shutdown Command and is shutting down now");
-
- if (pid != null) {
- waitForShutdown(pid, logger, statusFile, pidFile);
- }
- } else {
- logger.error("When sending SHUTDOWN command to NiFi, got unexpected response: {}", response);
- }
- } catch (final IOException ioe) {
- if (pid == null) {
- logger.error("Failed to send shutdown command to port {} due to {}. No PID found for the NiFi process, so unable to kill process; "
- + "the process should be killed manually.", port, ioe.toString());
- } else {
- logger.error("Failed to send shutdown command to port {} due to {}. Will kill the NiFi Process with PID {}.", port, ioe, pid);
- killProcessTree(pid, logger);
- if (statusFile.exists() && !statusFile.delete()) {
- logger.error("Failed to delete status file {}; this file should be cleaned up manually", statusFile);
- }
- }
- } finally {
- if (lockFile.exists() && !lockFile.delete()) {
- logger.error("Failed to delete lock file {}; this file should be cleaned up manually", lockFile);
- }
- }
- }
-
- private String readResponse(final InputStream in) throws IOException {
- int lastChar;
- final StringBuilder sb = new StringBuilder();
- while ((lastChar = in.read()) > -1) {
- sb.append((char) lastChar);
- }
-
- return sb.toString().trim();
- }
-
- private void waitForShutdown(final String pid, final Logger logger, final File statusFile, final File pidFile) throws IOException {
- final Properties bootstrapProperties = new Properties();
- try (final FileInputStream fis = new FileInputStream(bootstrapConfigFile)) {
- bootstrapProperties.load(fis);
- }
-
- String gracefulShutdown = bootstrapProperties.getProperty(GRACEFUL_SHUTDOWN_PROP, DEFAULT_GRACEFUL_SHUTDOWN_VALUE);
- int gracefulShutdownSeconds;
- try {
- gracefulShutdownSeconds = Integer.parseInt(gracefulShutdown);
- } catch (final NumberFormatException nfe) {
- gracefulShutdownSeconds = Integer.parseInt(DEFAULT_GRACEFUL_SHUTDOWN_VALUE);
- }
-
- final long startWait = System.nanoTime();
- while (isProcessRunning(pid, logger)) {
- logger.info("NiFi PID [{}] shutdown in progress...", pid);
- final long waitNanos = System.nanoTime() - startWait;
- final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
- if (waitSeconds >= gracefulShutdownSeconds && gracefulShutdownSeconds > 0) {
- if (isProcessRunning(pid, logger)) {
- logger.warn("NiFi PID [{}] shutdown not completed after {} seconds: Killing process", pid, gracefulShutdownSeconds);
- try {
- killProcessTree(pid, logger);
- } catch (final IOException ioe) {
- logger.error("Failed to kill Process with PID {}", pid);
- }
- }
- break;
- } else {
- try {
- Thread.sleep(GRACEFUL_SHUTDOWN_RETRY_MILLIS);
- } catch (final InterruptedException ie) {
- }
- }
- }
-
- if (statusFile.exists() && !statusFile.delete()) {
- logger.error("Failed to delete status file {}; this file should be cleaned up manually", statusFile);
- }
-
- if (pidFile.exists() && !pidFile.delete()) {
- logger.error("Failed to delete pid file {}; this file should be cleaned up manually", pidFile);
- }
-
- logger.info("NiFi PID [{}] shutdown completed", pid);
- }
-
- private static List getChildProcesses(final String ppid) throws IOException {
- final Process proc = Runtime.getRuntime().exec(new String[]{"ps", "-o", "pid", "--no-headers", "--ppid", ppid});
- final List childPids = new ArrayList<>();
- try (final InputStream in = proc.getInputStream();
- final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
-
- String line;
- while ((line = reader.readLine()) != null) {
- childPids.add(line.trim());
- }
- }
-
- return childPids;
- }
-
- private void killProcessTree(final String pid, final Logger logger) throws IOException {
- logger.debug("Killing Process Tree for PID {}", pid);
-
- final List children = getChildProcesses(pid);
- logger.debug("Children of PID {}: {}", pid, children);
-
- for (final String childPid : children) {
- killProcessTree(childPid, logger);
- }
-
- Runtime.getRuntime().exec(new String[]{"kill", "-9", pid});
- }
-
- public static boolean isAlive(final Process process) {
- try {
- process.exitValue();
- return false;
- } catch (final IllegalStateException | IllegalThreadStateException itse) {
- return true;
- }
- }
-
- @SuppressWarnings({"rawtypes", "unchecked"})
- public void start(final boolean monitor) throws IOException {
- final Integer port = getCurrentPort(cmdLogger);
- if (port != null) {
- cmdLogger.info("Apache NiFi is already running, listening to Bootstrap on port {}", port);
- return;
- }
-
- final File prevLockFile = getLockFile(cmdLogger);
- if (prevLockFile.exists() && !prevLockFile.delete()) {
- cmdLogger.warn("Failed to delete previous lock file {}; this file should be cleaned up manually", prevLockFile);
- }
-
- runtimeValidatorExecutor.execute();
-
- final ProcessBuilder builder = new ProcessBuilder();
-
- if (!bootstrapConfigFile.exists()) {
- throw new FileNotFoundException(bootstrapConfigFile.getAbsolutePath());
- }
-
- final Properties properties = new Properties();
- try (final FileInputStream fis = new FileInputStream(bootstrapConfigFile)) {
- properties.load(fis);
- }
-
- final Map props = new HashMap<>();
- props.putAll((Map) properties);
-
- final String specifiedWorkingDir = props.get("working.dir");
- if (specifiedWorkingDir != null) {
- builder.directory(new File(specifiedWorkingDir));
- }
-
- final File bootstrapConfigAbsoluteFile = bootstrapConfigFile.getAbsoluteFile();
- final File binDir = bootstrapConfigAbsoluteFile.getParentFile();
- final File workingDir = binDir.getParentFile();
-
- if (specifiedWorkingDir == null) {
- builder.directory(workingDir);
- }
-
- final String nifiLogDir = replaceNull(System.getProperty("org.apache.nifi.bootstrap.config.log.dir"), DEFAULT_LOG_DIR).trim();
-
- final String libFilename = replaceNull(props.get("lib.dir"), "./lib").trim();
- File libDir = getFile(libFilename, workingDir);
-
- final String confFilename = replaceNull(props.get("conf.dir"), "./conf").trim();
- File confDir = getFile(confFilename, workingDir);
-
- String nifiPropsFilename = props.get("props.file");
- if (nifiPropsFilename == null) {
- if (confDir.exists()) {
- nifiPropsFilename = new File(confDir, "nifi.properties").getAbsolutePath();
- } else {
- nifiPropsFilename = DEFAULT_CONFIG_FILE;
- }
- }
-
- nifiPropsFilename = nifiPropsFilename.trim();
-
- String maximumHeapSize = null;
- String minimumHeapSize = null;
-
- final List javaAdditionalArgs = new ArrayList<>();
- for (final Map.Entry entry : props.entrySet()) {
- final String key = entry.getKey();
- final String value = entry.getValue();
-
- if (key.startsWith("java.arg")) {
- javaAdditionalArgs.add(value);
- if (value.startsWith("-Xms")) {
- minimumHeapSize = value.replace("-Xms", "");
- }
- if (value.startsWith("-Xmx")) {
- maximumHeapSize = value.replace("-Xmx", "");
- }
- }
- }
-
- final File[] libFiles = libDir.listFiles(new FilenameFilter() {
- @Override
- public boolean accept(final File dir, final String filename) {
- return filename.toLowerCase().endsWith(".jar");
- }
- });
-
- if (libFiles == null || libFiles.length == 0) {
- throw new RuntimeException("Could not find lib directory at " + libDir.getAbsolutePath());
- }
-
- final File[] confFiles = confDir.listFiles();
- if (confFiles == null || confFiles.length == 0) {
- throw new RuntimeException("Could not find conf directory at " + confDir.getAbsolutePath());
- }
-
- final List cpFiles = new ArrayList<>(confFiles.length + libFiles.length);
- cpFiles.add(confDir.getAbsolutePath());
- for (final File file : libFiles) {
- cpFiles.add(file.getAbsolutePath());
- }
-
- defaultLogger.info(getPlatformDetails(minimumHeapSize, maximumHeapSize));
-
- final StringBuilder classPathBuilder = new StringBuilder();
- for (int i = 0; i < cpFiles.size(); i++) {
- final String filename = cpFiles.get(i);
- classPathBuilder.append(filename);
- if (i < cpFiles.size() - 1) {
- classPathBuilder.append(File.pathSeparatorChar);
- }
- }
-
- final String classPath = classPathBuilder.toString();
- String javaCmd = props.get("java");
- if (javaCmd == null) {
- javaCmd = DEFAULT_JAVA_CMD;
- }
- if (javaCmd.equals(DEFAULT_JAVA_CMD)) {
- String javaHome = System.getenv("JAVA_HOME");
- if (javaHome != null) {
- String fileExtension = isWindows() ? ".exe" : "";
- File javaFile = new File(javaHome + File.separatorChar + "bin"
- + File.separatorChar + "java" + fileExtension);
- if (javaFile.exists() && javaFile.canExecute()) {
- javaCmd = javaFile.getAbsolutePath();
- }
- }
- }
-
- try {
- final ApplicationPropertyHandler propertyHandler = new SecurityApplicationPropertyHandler(cmdLogger);
- final Path applicationPropertiesLocation = Paths.get(nifiPropsFilename);
- propertyHandler.handleProperties(applicationPropertiesLocation);
- } catch (final RuntimeException e) {
- cmdLogger.error("Self-Signed Certificate Generation Failed", e);
- }
-
- final String listenPortPropString = props.get(NIFI_BOOTSTRAP_LISTEN_PORT_PROP);
- int listenPortPropInt = 0; // default to zero (random ephemeral port)
- if (listenPortPropString != null) {
- try {
- listenPortPropInt = Integer.parseInt(listenPortPropString.trim());
- } catch (final Exception e) {
- // no-op, use the default
- }
- }
-
- final NiFiListener listener = new NiFiListener();
- final int listenPort = listener.start(this, listenPortPropInt);
-
- final List cmd = new ArrayList<>();
-
- cmd.add(javaCmd);
- cmd.add("-classpath");
- cmd.add(classPath);
- cmd.addAll(javaAdditionalArgs);
-
- cmd.add("-Dnifi.properties.file.path=" + nifiPropsFilename);
- cmd.add("-Dnifi.bootstrap.listen.port=" + listenPort);
- cmd.add("-Dapp=NiFi");
- cmd.add("-Dorg.apache.nifi.bootstrap.config.log.dir=" + nifiLogDir);
- cmd.add("org.apache.nifi.NiFi");
-
- builder.command(cmd);
-
- final StringBuilder cmdBuilder = new StringBuilder();
- for (final String s : cmd) {
- cmdBuilder.append(s).append(" ");
- }
-
- cmdLogger.info("Starting Apache NiFi...");
- cmdLogger.info("Working Directory: {}", workingDir.getAbsolutePath());
- cmdLogger.info("Command: {}", cmdBuilder);
-
- String gracefulShutdown = props.get(GRACEFUL_SHUTDOWN_PROP);
- if (gracefulShutdown == null) {
- gracefulShutdown = DEFAULT_GRACEFUL_SHUTDOWN_VALUE;
- }
-
- final int gracefulShutdownSeconds;
- try {
- gracefulShutdownSeconds = Integer.parseInt(gracefulShutdown);
- } catch (final NumberFormatException nfe) {
- throw new NumberFormatException("The '" + GRACEFUL_SHUTDOWN_PROP + "' property in Bootstrap Config File "
- + bootstrapConfigAbsoluteFile.getAbsolutePath() + " has an invalid value. Must be a non-negative integer");
- }
-
- if (gracefulShutdownSeconds < 0) {
- throw new NumberFormatException("The '" + GRACEFUL_SHUTDOWN_PROP + "' property in Bootstrap Config File "
- + bootstrapConfigAbsoluteFile.getAbsolutePath() + " has an invalid value. Must be a non-negative integer");
- }
-
- Process process = builder.start();
- handleLogging(process);
- nifiPid = process.pid();
- final Properties pidProperties = new Properties();
- pidProperties.setProperty(PID_KEY, String.valueOf(nifiPid));
- savePidProperties(pidProperties, cmdLogger);
- cmdLogger.info("Application Process [{}] launched", nifiPid);
-
- shutdownHook = new ShutdownHook(process, nifiPid, this, secretKey, gracefulShutdownSeconds, loggingExecutor);
-
- if (monitor) {
- final Runtime runtime = Runtime.getRuntime();
- runtime.addShutdownHook(shutdownHook);
-
- while (true) {
- final boolean alive = isAlive(process);
-
- if (alive) {
- try {
- Thread.sleep(1000L);
- } catch (final InterruptedException ie) {
- }
- } else {
- try {
- runtime.removeShutdownHook(shutdownHook);
- } catch (final IllegalStateException ise) {
- // happens when already shutting down
- }
-
- if (autoRestartNiFi) {
- final File statusFile = getStatusFile(defaultLogger);
- if (!statusFile.exists()) {
- defaultLogger.info("Status File no longer exists. Will not restart NiFi");
- return;
- }
-
- final File lockFile = getLockFile(defaultLogger);
- if (lockFile.exists()) {
- defaultLogger.info("A shutdown was initiated. Will not restart NiFi");
- return;
- }
-
- final boolean previouslyStarted = getNifiStarted();
- if (!previouslyStarted) {
- defaultLogger.info("NiFi never started. Will not restart NiFi");
- return;
- } else {
- setNiFiStarted(false);
- }
-
- defaultLogger.warn("Apache NiFi appears to have died. Restarting...");
- secretKey = null;
- process = builder.start();
- handleLogging(process);
-
- nifiPid = process.pid();
- pidProperties.setProperty(PID_KEY, String.valueOf(nifiPid));
- savePidProperties(pidProperties, defaultLogger);
- cmdLogger.info("Application Process [{}] launched", nifiPid);
-
- shutdownHook = new ShutdownHook(process, nifiPid, this, secretKey, gracefulShutdownSeconds, loggingExecutor);
- runtime.addShutdownHook(shutdownHook);
-
- final boolean started = waitForStart();
-
- if (started) {
- cmdLogger.info("Application Process [{}] started", nifiPid);
- } else {
- defaultLogger.error("Application Process [{}] not started", nifiPid);
- }
- } else {
- return;
- }
- }
- }
- }
- }
-
- private String getPlatformDetails(final String minimumHeapSize, final String maximumHeapSize) {
- final Map details = new LinkedHashMap(6);
-
- details.put("javaVersion", System.getProperty("java.version"));
- details.put("availableProcessors", Integer.toString(Runtime.getRuntime().availableProcessors()));
-
- try {
- final ObjectName osObjectName = ManagementFactory.getOperatingSystemMXBean().getObjectName();
- final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
- final Object maxOpenFileCount = mBeanServer.getAttribute(osObjectName, "MaxFileDescriptorCount");
- if (maxOpenFileCount != null) {
- details.put("maxOpenFileDescriptors", String.valueOf(maxOpenFileCount));
- }
- final Object totalPhysicalMemory = mBeanServer.getAttribute(osObjectName, "TotalPhysicalMemorySize");
- if (totalPhysicalMemory != null) {
- details.put("totalPhysicalMemoryMB", String.valueOf(((Long) totalPhysicalMemory) / (1024 * 1024)));
- }
- } catch (final Throwable t) {
- // Ignore. This will throw either ClassNotFound or NoClassDefFoundError if unavailable in this JVM.
- }
-
- if (minimumHeapSize != null) {
- details.put("minimumHeapSize", minimumHeapSize);
- }
- if (maximumHeapSize != null) {
- details.put("maximumHeapSize", maximumHeapSize);
- }
-
- return details.toString();
- }
-
- private void handleLogging(final Process process) {
- final Set> existingFutures = loggingFutures;
- if (existingFutures != null) {
- for (final Future> future : existingFutures) {
- future.cancel(false);
- }
- }
-
- final Future> stdOutFuture = loggingExecutor.submit(new Runnable() {
- @Override
- public void run() {
- final Logger stdOutLogger = LoggerFactory.getLogger("org.apache.nifi.StdOut");
- final InputStream in = process.getInputStream();
- try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
- String line;
- while ((line = reader.readLine()) != null) {
- stdOutLogger.info(line);
- }
- } catch (IOException e) {
- defaultLogger.error("Failed to read from NiFi's Standard Out stream", e);
- }
- }
- });
-
- final Future> stdErrFuture = loggingExecutor.submit(new Runnable() {
- @Override
- public void run() {
- final Logger stdErrLogger = LoggerFactory.getLogger("org.apache.nifi.StdErr");
- final InputStream in = process.getErrorStream();
- try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
- String line;
- while ((line = reader.readLine()) != null) {
- stdErrLogger.error(line);
- }
- } catch (IOException e) {
- defaultLogger.error("Failed to read from NiFi's Standard Error stream", e);
- }
- }
- });
-
- final Set> futures = new HashSet<>();
- futures.add(stdOutFuture);
- futures.add(stdErrFuture);
- this.loggingFutures = futures;
- }
-
-
- private boolean isWindows() {
- final String osName = System.getProperty("os.name");
- return osName != null && osName.toLowerCase().contains("win");
- }
-
- private boolean waitForStart() {
- lock.lock();
- try {
- final long startTime = System.nanoTime();
-
- while (ccPort < 1) {
- try {
- startupCondition.await(1, TimeUnit.SECONDS);
- } catch (final InterruptedException ie) {
- return false;
- }
-
- final long waitNanos = System.nanoTime() - startTime;
- final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
- if (waitSeconds > STARTUP_WAIT_SECONDS) {
- return false;
- }
- }
- } finally {
- lock.unlock();
- }
- return true;
- }
-
- private File getFile(final String filename, final File workingDir) {
- File file = new File(filename);
- if (!file.isAbsolute()) {
- file = new File(workingDir, filename);
- }
-
- return file;
- }
-
- private String replaceNull(final String value, final String replacement) {
- return (value == null) ? replacement : value;
- }
-
- void setAutoRestartNiFi(final boolean restart) {
- this.autoRestartNiFi = restart;
- }
-
- void setNiFiCommandControlPort(final int port, final String secretKey) throws IOException {
-
- if (this.secretKey != null && this.ccPort != UNINITIALIZED_CC_PORT) {
- defaultLogger.warn("Blocking attempt to change NiFi command port and secret after they have already been initialized. requestedPort={}", port);
- return;
- }
-
- this.ccPort = port;
- this.secretKey = secretKey;
-
- if (shutdownHook != null) {
- shutdownHook.setSecretKey(secretKey);
- }
-
- final File statusFile = getStatusFile(defaultLogger);
-
- final Properties nifiProps = new Properties();
- if (nifiPid != -1) {
- nifiProps.setProperty(PID_KEY, String.valueOf(nifiPid));
- }
- nifiProps.setProperty("port", String.valueOf(ccPort));
- nifiProps.setProperty("secret.key", secretKey);
-
- try {
- savePidProperties(nifiProps, defaultLogger);
- } catch (final IOException ioe) {
- defaultLogger.warn("Apache NiFi has started but failed to persist NiFi Port information to {} due to {}", statusFile.getAbsolutePath(), ioe);
- }
-
- defaultLogger.info("Apache NiFi now running and listening for Bootstrap requests on port {}", port);
- }
-
- int getNiFiCommandControlPort() {
- return this.ccPort;
- }
-
- void setNiFiStarted(final boolean nifiStarted) {
- startedLock.lock();
- try {
- this.nifiStarted = nifiStarted;
- } finally {
- startedLock.unlock();
- }
- }
-
- boolean getNifiStarted() {
- startedLock.lock();
- try {
- return nifiStarted;
- } finally {
- startedLock.unlock();
- }
- }
-
- private static class Status {
-
- private final Integer port;
- private final String pid;
-
- private final Boolean respondingToPing;
- private final Boolean processRunning;
-
- public Status(final Integer port, final String pid, final Boolean respondingToPing, final Boolean processRunning) {
- this.port = port;
- this.pid = pid;
- this.respondingToPing = respondingToPing;
- this.processRunning = processRunning;
- }
-
- public String getPid() {
- return pid;
- }
-
- public Integer getPort() {
- return port;
- }
-
- public boolean isRespondingToPing() {
- return Boolean.TRUE.equals(respondingToPing);
- }
-
- public boolean isProcessRunning() {
- return Boolean.TRUE.equals(processRunning);
- }
- }
-
- private static class NiFiNotRunningException extends Exception {
- @Override
- public synchronized Throwable fillInStackTrace() {
- return this;
- }
- }
-}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/ShutdownHook.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/ShutdownHook.java
deleted file mode 100644
index 502834c3c0c9..000000000000
--- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/ShutdownHook.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.bootstrap;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-
-public class ShutdownHook extends Thread {
-
- private final Process nifiProcess;
- private final Long pid;
- private final RunNiFi runner;
- private final int gracefulShutdownSeconds;
- private final ExecutorService executor;
-
- private volatile String secretKey;
-
- public ShutdownHook(final Process nifiProcess, final Long pid, final RunNiFi runner, final String secretKey, final int gracefulShutdownSeconds, final ExecutorService executor) {
- this.nifiProcess = nifiProcess;
- this.pid = pid;
- this.runner = runner;
- this.secretKey = secretKey;
- this.gracefulShutdownSeconds = gracefulShutdownSeconds;
- this.executor = executor;
- }
-
- void setSecretKey(final String secretKey) {
- this.secretKey = secretKey;
- }
-
- @Override
- public void run() {
- executor.shutdown();
- runner.setAutoRestartNiFi(false);
- final int ccPort = runner.getNiFiCommandControlPort();
- if (ccPort > 0) {
- System.out.printf("NiFi PID [%d] shutdown started%n", pid);
-
- try {
- final Socket socket = new Socket("localhost", ccPort);
- final OutputStream out = socket.getOutputStream();
- out.write(("SHUTDOWN " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
- out.flush();
-
- socket.close();
- } catch (final IOException ioe) {
- System.out.println("Failed to Shutdown NiFi due to " + ioe);
- }
- }
-
- System.out.printf("NiFi PID [%d] shutdown in progress...%n", pid);
- final long startWait = System.nanoTime();
- while (RunNiFi.isAlive(nifiProcess)) {
- final long waitNanos = System.nanoTime() - startWait;
- final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
- if (waitSeconds >= gracefulShutdownSeconds && gracefulShutdownSeconds > 0) {
- if (RunNiFi.isAlive(nifiProcess)) {
- System.out.println("NiFi has not finished shutting down after " + gracefulShutdownSeconds + " seconds. Killing process.");
- nifiProcess.destroy();
- }
- break;
- } else {
- try {
- Thread.sleep(1000L);
- } catch (final InterruptedException ie) {
- }
- }
- }
-
- try {
- final File statusFile = runner.getStatusFile();
- if (!statusFile.delete()) {
- System.err.println("Failed to delete status file " + statusFile.getAbsolutePath() + "; this file should be cleaned up manually");
- }
- } catch (IOException ex) {
- System.err.println("Failed to retrieve status file " + ex);
- }
- }
-}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/ApplicationProcessStatusBootstrapCommand.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/ApplicationProcessStatusBootstrapCommand.java
new file mode 100644
index 000000000000..6abbd97af167
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/ApplicationProcessStatusBootstrapCommand.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Bootstrap Command to return the status of the application process as a child of the bootstrap process
+ */
+class ApplicationProcessStatusBootstrapCommand implements BootstrapCommand {
+
+ private static final Logger logger = LoggerFactory.getLogger(ApplicationProcessStatusBootstrapCommand.class);
+
+ private final ProcessHandle processHandle;
+
+ private CommandStatus commandStatus = CommandStatus.ERROR;
+
+ ApplicationProcessStatusBootstrapCommand(final ProcessHandle processHandle) {
+ this.processHandle = Objects.requireNonNull(processHandle);
+ }
+
+ @Override
+ public CommandStatus getCommandStatus() {
+ return commandStatus;
+ }
+
+ @Override
+ public void run() {
+ final Optional childProcessHandleFound = processHandle.children().findFirst();
+
+ if (childProcessHandleFound.isEmpty()) {
+ logger.info("Application Process not found");
+ commandStatus = CommandStatus.STOPPED;
+ } else {
+ final ProcessHandle childProcessHandle = childProcessHandleFound.get();
+ if (childProcessHandle.isAlive()) {
+ logger.info("Application Process [{}] running", childProcessHandle.pid());
+ commandStatus = CommandStatus.SUCCESS;
+ } else {
+ logger.info("Application Process [{}] stopped", childProcessHandle.pid());
+ commandStatus = CommandStatus.COMMUNICATION_FAILED;
+ }
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/BootstrapCommand.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/BootstrapCommand.java
new file mode 100644
index 000000000000..25833a3ac968
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/BootstrapCommand.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+/**
+ * Bootstrap Command extension of Runnable with command status
+ */
+public interface BootstrapCommand extends Runnable {
+ /**
+ * Get Command Status on completion
+ *
+ * @return Command Status
+ */
+ CommandStatus getCommandStatus();
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/BootstrapCommandProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/BootstrapCommandProvider.java
new file mode 100644
index 000000000000..18a0f2191aae
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/BootstrapCommandProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+/**
+ * Abstraction for parsing arguments and returning runnable Bootstrap Command
+ */
+public interface BootstrapCommandProvider {
+ /**
+ * Get Bootstrap Command
+ *
+ * @param arguments Application arguments
+ * @return Bootstrap Command to run
+ */
+ BootstrapCommand getBootstrapCommand(String[] arguments);
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/CommandStatus.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/CommandStatus.java
new file mode 100644
index 000000000000..b2d050f0f974
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/CommandStatus.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+/**
+ * Enumeration of Bootstrap Command Statuses with status codes
+ */
+public enum CommandStatus {
+ RUNNING(-1),
+
+ SUCCESS(0),
+
+ ERROR(1),
+
+ STOPPED(3),
+
+ COMMUNICATION_FAILED(4),
+
+ FAILED(5);
+
+ private final int status;
+
+ CommandStatus(final int status) {
+ this.status = status;
+ }
+
+ /**
+ * Get Status Code for use with System.exit()
+ *
+ * @return Status Code
+ */
+ public int getStatus() {
+ return status;
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/GetRunCommandBootstrapCommand.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/GetRunCommandBootstrapCommand.java
new file mode 100644
index 000000000000..b854d6ded554
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/GetRunCommandBootstrapCommand.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.apache.nifi.bootstrap.command.process.ManagementServerAddressProvider;
+import org.apache.nifi.bootstrap.command.process.ProcessBuilderProvider;
+import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
+import org.apache.nifi.bootstrap.command.process.StandardManagementServerAddressProvider;
+import org.apache.nifi.bootstrap.command.process.StandardProcessBuilderProvider;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.apache.nifi.bootstrap.property.ApplicationPropertyHandler;
+import org.apache.nifi.bootstrap.property.SecurityApplicationPropertyHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Bootstrap Command to get the command arguments for running the application
+ */
+class GetRunCommandBootstrapCommand implements BootstrapCommand {
+
+ private static final String SPACE_SEPARATOR = " ";
+
+ private static final Logger logger = LoggerFactory.getLogger(GetRunCommandBootstrapCommand.class);
+
+ private final ConfigurationProvider configurationProvider;
+
+ private final ProcessHandleProvider processHandleProvider;
+
+ private final ManagementServerAddressProvider managementServerAddressProvider;
+
+ private final PrintStream outputStream;
+
+ private CommandStatus commandStatus = CommandStatus.ERROR;
+
+ public GetRunCommandBootstrapCommand(final ConfigurationProvider configurationProvider, final ProcessHandleProvider processHandleProvider, final PrintStream outputStream) {
+ this.configurationProvider = Objects.requireNonNull(configurationProvider);
+ this.processHandleProvider = Objects.requireNonNull(processHandleProvider);
+ this.outputStream = Objects.requireNonNull(outputStream);
+ this.managementServerAddressProvider = new StandardManagementServerAddressProvider(configurationProvider);
+ }
+
+ @Override
+ public CommandStatus getCommandStatus() {
+ return commandStatus;
+ }
+
+ @Override
+ public void run() {
+ try {
+ final Optional applicationProcessHandle = processHandleProvider.findApplicationProcessHandle();
+
+ if (applicationProcessHandle.isEmpty()) {
+ final ApplicationPropertyHandler securityApplicationPropertyHandler = new SecurityApplicationPropertyHandler(logger);
+ securityApplicationPropertyHandler.handleProperties(configurationProvider.getApplicationProperties());
+
+ final ProcessBuilderProvider processBuilderProvider = new StandardProcessBuilderProvider(configurationProvider, managementServerAddressProvider);
+ final ProcessBuilder processBuilder = processBuilderProvider.getApplicationProcessBuilder();
+ final List command = processBuilder.command();
+ final String processCommand = String.join(SPACE_SEPARATOR, command);
+ outputStream.println(processCommand);
+
+ commandStatus = CommandStatus.SUCCESS;
+ } else {
+ logger.info("Application Process [{}] running", applicationProcessHandle.get().pid());
+ commandStatus = CommandStatus.ERROR;
+ }
+ } catch (final Throwable e) {
+ logger.warn("Application Process command building failed", e);
+ commandStatus = CommandStatus.FAILED;
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/ManagementServerBootstrapCommand.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/ManagementServerBootstrapCommand.java
new file mode 100644
index 000000000000..e462249ac2eb
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/ManagementServerBootstrapCommand.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.apache.nifi.bootstrap.command.io.HttpRequestMethod;
+import org.apache.nifi.bootstrap.command.io.ResponseStreamHandler;
+import org.apache.nifi.bootstrap.command.process.ManagementServerAddressProvider;
+import org.apache.nifi.bootstrap.command.process.ProcessHandleManagementServerAddressProvider;
+import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
+import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
+import org.apache.nifi.bootstrap.configuration.ManagementServerPath;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Sequence of Bootstrap Commands
+ */
+class ManagementServerBootstrapCommand implements BootstrapCommand {
+
+ private static final Logger commandLogger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
+
+ private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
+
+ private static final Duration READ_TIMEOUT = Duration.ofSeconds(15);
+
+ private static final String SERVER_URI = "http://%s%s";
+
+ private static final char QUERY_SEPARATOR = '?';
+
+ private final ProcessHandleProvider processHandleProvider;
+
+ private final HttpRequestMethod httpRequestMethod;
+
+ private final ManagementServerPath managementServerPath;
+
+ private final String managementServerQuery;
+
+ private final int successStatusCode;
+
+ private final ResponseStreamHandler responseStreamHandler;
+
+ private CommandStatus commandStatus = CommandStatus.ERROR;
+
+ ManagementServerBootstrapCommand(
+ final ProcessHandleProvider processHandleProvider,
+ final ManagementServerPath managementServerPath,
+ final ResponseStreamHandler responseStreamHandler
+ ) {
+ this(processHandleProvider, HttpRequestMethod.GET, managementServerPath, null, HttpURLConnection.HTTP_OK, responseStreamHandler);
+ }
+
+ ManagementServerBootstrapCommand(
+ final ProcessHandleProvider processHandleProvider,
+ final HttpRequestMethod httpRequestMethod,
+ final ManagementServerPath managementServerPath,
+ final String managementServerQuery,
+ final int successStatusCode,
+ final ResponseStreamHandler responseStreamHandler
+ ) {
+ this.processHandleProvider = Objects.requireNonNull(processHandleProvider);
+ this.httpRequestMethod = Objects.requireNonNull(httpRequestMethod);
+ this.managementServerPath = Objects.requireNonNull(managementServerPath);
+ this.managementServerQuery = managementServerQuery;
+ this.successStatusCode = successStatusCode;
+ this.responseStreamHandler = Objects.requireNonNull(responseStreamHandler);
+ }
+
+ @Override
+ public CommandStatus getCommandStatus() {
+ return commandStatus;
+ }
+
+ @Override
+ public void run() {
+ final Optional applicationProcessHandle = processHandleProvider.findApplicationProcessHandle();
+
+ if (applicationProcessHandle.isEmpty()) {
+ commandStatus = CommandStatus.STOPPED;
+ getCommandLogger().info("Application Process STOPPED");
+ } else {
+ run(applicationProcessHandle.get());
+ }
+ }
+
+ protected void run(final ProcessHandle applicationProcessHandle) {
+ final ManagementServerAddressProvider managementServerAddressProvider = new ProcessHandleManagementServerAddressProvider(applicationProcessHandle);
+ final Optional managementServerAddress = managementServerAddressProvider.getAddress();
+
+ final long pid = applicationProcessHandle.pid();
+ if (managementServerAddress.isEmpty()) {
+ getCommandLogger().info("Application Process [{}] Management Server address not found", pid);
+ commandStatus = CommandStatus.ERROR;
+ } else {
+ final URI managementServerUri = getManagementServerUri(managementServerAddress.get());
+ try (HttpClient httpClient = getHttpClient()) {
+ final HttpRequest httpRequest = getHttpRequest(managementServerUri);
+
+ final HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofInputStream());
+ final int statusCode = response.statusCode();
+ try (InputStream responseStream = response.body()) {
+ onResponseStatus(applicationProcessHandle, statusCode, responseStream);
+ }
+ } catch (final Exception e) {
+ commandStatus = CommandStatus.COMMUNICATION_FAILED;
+ getCommandLogger().info("Application Process [{}] Management Server [{}] communication failed", pid, managementServerUri);
+ }
+ }
+ }
+
+ protected void onResponseStatus(final ProcessHandle applicationProcessHandle, final int statusCode, final InputStream responseStream) {
+ final long pid = applicationProcessHandle.pid();
+
+ if (successStatusCode == statusCode) {
+ commandStatus = CommandStatus.SUCCESS;
+ getCommandLogger().info("Application Process [{}] Command Status [{}] HTTP {}", pid, commandStatus, statusCode);
+ responseStreamHandler.onResponseStream(responseStream);
+ } else {
+ commandStatus = CommandStatus.COMMUNICATION_FAILED;
+ getCommandLogger().warn("Application Process [{}] Command Status [{}] HTTP {}", pid, commandStatus, statusCode);
+ }
+ }
+
+ protected Logger getCommandLogger() {
+ return commandLogger;
+ }
+
+ protected HttpRequest getHttpRequest(final URI managementServerUri) {
+ return HttpRequest.newBuilder()
+ .method(httpRequestMethod.name(), HttpRequest.BodyPublishers.noBody())
+ .uri(managementServerUri)
+ .timeout(READ_TIMEOUT)
+ .build();
+ }
+
+ protected URI getManagementServerUri(final String managementServerAddress) {
+ final StringBuilder builder = new StringBuilder();
+
+ final String serverUri = SERVER_URI.formatted(managementServerAddress, managementServerPath.getPath());
+ builder.append(serverUri);
+
+ if (managementServerQuery != null) {
+ builder.append(QUERY_SEPARATOR);
+ builder.append(managementServerQuery);
+ }
+
+ return URI.create(builder.toString());
+ }
+
+ protected HttpClient getHttpClient() {
+ final HttpClient.Builder builder = HttpClient.newBuilder();
+ builder.connectTimeout(CONNECT_TIMEOUT);
+ return builder.build();
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/RunBootstrapCommand.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/RunBootstrapCommand.java
new file mode 100644
index 000000000000..a2be6549faca
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/RunBootstrapCommand.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import com.sun.management.UnixOperatingSystemMXBean;
+import org.apache.nifi.bootstrap.command.process.ManagementServerAddressProvider;
+import org.apache.nifi.bootstrap.command.process.ProcessBuilderProvider;
+import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
+import org.apache.nifi.bootstrap.command.process.StandardManagementServerAddressProvider;
+import org.apache.nifi.bootstrap.command.process.StandardProcessBuilderProvider;
+import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.apache.nifi.bootstrap.process.RuntimeValidatorExecutor;
+import org.apache.nifi.bootstrap.property.ApplicationPropertyHandler;
+import org.apache.nifi.bootstrap.property.SecurityApplicationPropertyHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.OperatingSystemMXBean;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Bootstrap Command to run the application
+ */
+class RunBootstrapCommand implements BootstrapCommand {
+
+ private static final String SPACE_SEPARATOR = " ";
+
+ private static final Logger commandLogger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
+
+ private static final Logger logger = LoggerFactory.getLogger(RunBootstrapCommand.class);
+
+ private static final RuntimeValidatorExecutor runtimeValidatorExecutor = new RuntimeValidatorExecutor();
+
+ private final ConfigurationProvider configurationProvider;
+
+ private final ProcessHandleProvider processHandleProvider;
+
+ private final ManagementServerAddressProvider managementServerAddressProvider;
+
+ private CommandStatus commandStatus = CommandStatus.ERROR;
+
+ public RunBootstrapCommand(final ConfigurationProvider configurationProvider, final ProcessHandleProvider processHandleProvider) {
+ this.configurationProvider = Objects.requireNonNull(configurationProvider);
+ this.processHandleProvider = Objects.requireNonNull(processHandleProvider);
+ this.managementServerAddressProvider = new StandardManagementServerAddressProvider(configurationProvider);
+ }
+
+ @Override
+ public CommandStatus getCommandStatus() {
+ return commandStatus;
+ }
+
+ @Override
+ public void run() {
+ try {
+ final Optional applicationProcessHandle = processHandleProvider.findApplicationProcessHandle();
+
+ if (applicationProcessHandle.isEmpty()) {
+ writePlatformProperties();
+
+ runtimeValidatorExecutor.execute();
+
+ final ApplicationPropertyHandler securityApplicationPropertyHandler = new SecurityApplicationPropertyHandler(logger);
+ securityApplicationPropertyHandler.handleProperties(configurationProvider.getApplicationProperties());
+
+ final ProcessBuilderProvider processBuilderProvider = new StandardProcessBuilderProvider(configurationProvider, managementServerAddressProvider);
+
+ final ProcessBuilder processBuilder = processBuilderProvider.getApplicationProcessBuilder();
+ processBuilder.inheritIO();
+
+ final String command = String.join(SPACE_SEPARATOR, processBuilder.command());
+ logger.info(command);
+
+ final Process process = processBuilder.start();
+ if (process.isAlive()) {
+ commandStatus = CommandStatus.SUCCESS;
+ commandLogger.info("Application Process [{}] started", process.pid());
+ } else {
+ commandStatus = CommandStatus.STOPPED;
+ commandLogger.error("Application Process [{}] start failed", process.pid());
+ }
+ } else {
+ commandLogger.info("Application Process [{}] running", applicationProcessHandle.get().pid());
+ commandStatus = CommandStatus.ERROR;
+ }
+ } catch (final Throwable e) {
+ commandLogger.warn("Application Process run failed", e);
+ commandStatus = CommandStatus.FAILED;
+ }
+ }
+
+ private void writePlatformProperties() {
+ final Runtime.Version version = Runtime.version();
+ logger.info("Java Version: {}", version);
+
+ final Runtime runtime = Runtime.getRuntime();
+ logger.info("Available Processors: {}", runtime.availableProcessors());
+
+ final OperatingSystemMXBean operatingSystem = ManagementFactory.getOperatingSystemMXBean();
+ if (operatingSystem instanceof UnixOperatingSystemMXBean unixOperatingSystem) {
+ logger.info("Total Memory: {}", unixOperatingSystem.getTotalMemorySize());
+ logger.info("Maximum File Descriptors: {}", unixOperatingSystem.getMaxFileDescriptorCount());
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/SequenceBootstrapCommand.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/SequenceBootstrapCommand.java
new file mode 100644
index 000000000000..f3d11c1cc2f1
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/SequenceBootstrapCommand.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Sequence of Bootstrap Commands
+ */
+class SequenceBootstrapCommand implements BootstrapCommand {
+
+ private final List bootstrapCommands;
+
+ private CommandStatus commandStatus = CommandStatus.ERROR;
+
+ SequenceBootstrapCommand(final List bootstrapCommands) {
+ this.bootstrapCommands = Objects.requireNonNull(bootstrapCommands);
+ }
+
+ @Override
+ public CommandStatus getCommandStatus() {
+ return commandStatus;
+ }
+
+ @Override
+ public void run() {
+ for (final BootstrapCommand bootstrapCommand : bootstrapCommands) {
+ bootstrapCommand.run();
+ commandStatus = bootstrapCommand.getCommandStatus();
+
+ if (CommandStatus.SUCCESS != commandStatus) {
+ break;
+ }
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/StandardBootstrapCommandProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/StandardBootstrapCommandProvider.java
new file mode 100644
index 000000000000..bfaada97b454
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/StandardBootstrapCommandProvider.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.apache.nifi.bootstrap.command.io.BootstrapArgument;
+import org.apache.nifi.bootstrap.command.io.BootstrapArgumentParser;
+import org.apache.nifi.bootstrap.command.io.FileResponseStreamHandler;
+import org.apache.nifi.bootstrap.command.io.LoggerResponseStreamHandler;
+import org.apache.nifi.bootstrap.command.io.ResponseStreamHandler;
+import org.apache.nifi.bootstrap.command.io.StandardBootstrapArgumentParser;
+import org.apache.nifi.bootstrap.command.process.StandardProcessHandleProvider;
+import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
+import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.apache.nifi.bootstrap.configuration.StandardConfigurationProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static java.net.HttpURLConnection.HTTP_ACCEPTED;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.apache.nifi.bootstrap.command.io.HttpRequestMethod.DELETE;
+import static org.apache.nifi.bootstrap.command.io.HttpRequestMethod.GET;
+import static org.apache.nifi.bootstrap.configuration.ManagementServerPath.HEALTH;
+import static org.apache.nifi.bootstrap.configuration.ManagementServerPath.HEALTH_CLUSTER;
+import static org.apache.nifi.bootstrap.configuration.ManagementServerPath.HEALTH_DIAGNOSTICS;
+import static org.apache.nifi.bootstrap.configuration.ManagementServerPath.HEALTH_STATUS_HISTORY;
+
+/**
+ * Standard implementation of Bootstrap Command Provider with parsing of supported commands
+ */
+public class StandardBootstrapCommandProvider implements BootstrapCommandProvider {
+ private static final String SHUTDOWN_REQUESTED = "--shutdown=true";
+
+ private static final String VERBOSE_REQUESTED = "--verbose";
+
+ private static final String VERBOSE_QUERY = "verbose=true";
+
+ private static final String DAYS_QUERY = "days=%d";
+
+ private static final String EMPTY_QUERY = null;
+
+ private static final int FIRST_ARGUMENT = 1;
+
+ private static final int SECOND_ARGUMENT = 2;
+
+ private static final int PATH_ARGUMENTS = 2;
+
+ private static final int DAYS_PATH_ARGUMENTS = 3;
+
+ private static final int DAYS_REQUESTED_DEFAULT = 1;
+
+ private static final BootstrapArgumentParser bootstrapArgumentParser = new StandardBootstrapArgumentParser();
+
+ private static final Logger commandLogger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
+
+ /**
+ * Get Bootstrap Command
+ *
+ * @param arguments Application arguments
+ * @return Bootstrap Command to run
+ */
+ @Override
+ public BootstrapCommand getBootstrapCommand(final String[] arguments) {
+ final BootstrapCommand bootstrapCommand;
+
+ final Optional bootstrapArgumentFound = bootstrapArgumentParser.getBootstrapArgument(arguments);
+ if (bootstrapArgumentFound.isPresent()) {
+ final BootstrapArgument bootstrapArgument = bootstrapArgumentFound.get();
+ bootstrapCommand = getBootstrapCommand(bootstrapArgument, arguments);
+ } else {
+ bootstrapCommand = new UnknownBootstrapCommand();
+ }
+
+ return bootstrapCommand;
+ }
+
+ private BootstrapCommand getBootstrapCommand(final BootstrapArgument bootstrapArgument, final String[] arguments) {
+ final ConfigurationProvider configurationProvider = new StandardConfigurationProvider(System.getenv(), System.getProperties());
+ final ProcessHandleProvider processHandleProvider = new StandardProcessHandleProvider(configurationProvider);
+ final ResponseStreamHandler commandLoggerStreamHandler = new LoggerResponseStreamHandler(commandLogger);
+ final BootstrapCommand stopBootstrapCommand = new StopBootstrapCommand(processHandleProvider, configurationProvider);
+
+ final BootstrapCommand bootstrapCommand;
+
+ if (BootstrapArgument.CLUSTER_STATUS == bootstrapArgument) {
+ bootstrapCommand = new ManagementServerBootstrapCommand(processHandleProvider, HEALTH_CLUSTER, commandLoggerStreamHandler);
+ } else if (BootstrapArgument.DECOMMISSION == bootstrapArgument) {
+ bootstrapCommand = getDecommissionCommand(processHandleProvider, stopBootstrapCommand, arguments);
+ } else if (BootstrapArgument.DIAGNOSTICS == bootstrapArgument) {
+ bootstrapCommand = getDiagnosticsCommand(processHandleProvider, arguments);
+ } else if (BootstrapArgument.GET_RUN_COMMAND == bootstrapArgument) {
+ bootstrapCommand = new GetRunCommandBootstrapCommand(configurationProvider, processHandleProvider, System.out);
+ } else if (BootstrapArgument.START == bootstrapArgument) {
+ final BootstrapCommand runBootstrapCommand = new RunBootstrapCommand(configurationProvider, processHandleProvider);
+ final ProcessHandle currentProcessHandle = ProcessHandle.current();
+ final BootstrapCommand statusBootstrapCommand = new ApplicationProcessStatusBootstrapCommand(currentProcessHandle);
+ bootstrapCommand = new StartBootstrapCommand(runBootstrapCommand, statusBootstrapCommand);
+ } else if (BootstrapArgument.STATUS == bootstrapArgument) {
+ bootstrapCommand = new ManagementServerBootstrapCommand(processHandleProvider, HEALTH, commandLoggerStreamHandler);
+ } else if (BootstrapArgument.STATUS_HISTORY == bootstrapArgument) {
+ bootstrapCommand = getStatusHistoryCommand(processHandleProvider, arguments);
+ } else if (BootstrapArgument.STOP == bootstrapArgument) {
+ bootstrapCommand = stopBootstrapCommand;
+ } else {
+ bootstrapCommand = new UnknownBootstrapCommand();
+ }
+
+ return bootstrapCommand;
+ }
+
+ private BootstrapCommand getDecommissionCommand(final ProcessHandleProvider processHandleProvider, final BootstrapCommand stopBootstrapCommand, final String[] arguments) {
+ final ResponseStreamHandler responseStreamHandler = new LoggerResponseStreamHandler(commandLogger);
+ final List bootstrapCommands = new ArrayList<>();
+ final BootstrapCommand decommissionCommand = new ManagementServerBootstrapCommand(processHandleProvider, DELETE, HEALTH_CLUSTER, EMPTY_QUERY, HTTP_ACCEPTED, responseStreamHandler);
+ bootstrapCommands.add(decommissionCommand);
+ if (isShutdownRequested(arguments)) {
+ bootstrapCommands.add(stopBootstrapCommand);
+ }
+ return new SequenceBootstrapCommand(bootstrapCommands);
+ }
+
+ private BootstrapCommand getDiagnosticsCommand(final ProcessHandleProvider processHandleProvider, final String[] arguments) {
+ final String verboseQuery = getVerboseQuery(arguments);
+ final ResponseStreamHandler responseStreamHandler = getDiagnosticsResponseStreamHandler(arguments);
+ return new ManagementServerBootstrapCommand(processHandleProvider, GET, HEALTH_DIAGNOSTICS, verboseQuery, HTTP_OK, responseStreamHandler);
+ }
+
+ private ResponseStreamHandler getDiagnosticsResponseStreamHandler(final String[] arguments) {
+ final ResponseStreamHandler responseStreamHandler;
+
+ if (arguments.length == PATH_ARGUMENTS) {
+ final String outputPathArgument = arguments[FIRST_ARGUMENT];
+ final Path outputPath = Paths.get(outputPathArgument);
+ responseStreamHandler = new FileResponseStreamHandler(outputPath);
+ } else {
+ final Logger logger = LoggerFactory.getLogger(StandardBootstrapCommandProvider.class);
+ responseStreamHandler = new LoggerResponseStreamHandler(logger);
+ }
+
+ return responseStreamHandler;
+ }
+
+ private BootstrapCommand getStatusHistoryCommand(final ProcessHandleProvider processHandleProvider, final String[] arguments) {
+ final String daysQuery = getStatusHistoryDaysQuery(arguments);
+ final ResponseStreamHandler responseStreamHandler = getStatusHistoryResponseStreamHandler(arguments);
+ return new ManagementServerBootstrapCommand(processHandleProvider, GET, HEALTH_STATUS_HISTORY, daysQuery, HTTP_OK, responseStreamHandler);
+ }
+
+ private boolean isShutdownRequested(final String[] arguments) {
+ boolean shutdownRequested = false;
+
+ for (final String argument : arguments) {
+ if (SHUTDOWN_REQUESTED.contentEquals(argument)) {
+ shutdownRequested = true;
+ break;
+ }
+ }
+
+ return shutdownRequested;
+ }
+
+ private String getVerboseQuery(final String[] arguments) {
+ String query = null;
+
+ for (final String argument : arguments) {
+ if (VERBOSE_REQUESTED.contentEquals(argument)) {
+ query = VERBOSE_QUERY;
+ break;
+ }
+ }
+
+ return query;
+ }
+
+ private String getStatusHistoryDaysQuery(final String[] arguments) {
+ final int daysRequested;
+
+ if (arguments.length == DAYS_PATH_ARGUMENTS) {
+ final String daysRequestArgument = arguments[FIRST_ARGUMENT];
+ daysRequested = getStatusHistoryDaysRequested(daysRequestArgument);
+ } else {
+ daysRequested = DAYS_REQUESTED_DEFAULT;
+ }
+
+ return DAYS_QUERY.formatted(daysRequested);
+ }
+
+ private int getStatusHistoryDaysRequested(final String daysRequestArgument) {
+ int daysRequested;
+
+ try {
+ daysRequested = Integer.parseInt(daysRequestArgument);
+ } catch (final NumberFormatException e) {
+ throw new IllegalArgumentException("Status History Days requested not valid");
+ }
+
+ return daysRequested;
+ }
+
+ private ResponseStreamHandler getStatusHistoryResponseStreamHandler(final String[] arguments) {
+ final ResponseStreamHandler responseStreamHandler;
+
+ if (arguments.length == PATH_ARGUMENTS) {
+ final String outputPathArgument = arguments[FIRST_ARGUMENT];
+ final Path outputPath = Paths.get(outputPathArgument);
+ responseStreamHandler = new FileResponseStreamHandler(outputPath);
+ } else if (arguments.length == DAYS_PATH_ARGUMENTS) {
+ final String outputPathArgument = arguments[SECOND_ARGUMENT];
+ final Path outputPath = Paths.get(outputPathArgument);
+ responseStreamHandler = new FileResponseStreamHandler(outputPath);
+ } else {
+ final Logger logger = LoggerFactory.getLogger(StandardBootstrapCommandProvider.class);
+ responseStreamHandler = new LoggerResponseStreamHandler(logger);
+ }
+
+ return responseStreamHandler;
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/StartBootstrapCommand.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/StartBootstrapCommand.java
new file mode 100644
index 000000000000..0c49e0800e3b
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/StartBootstrapCommand.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Start Bootstrap Command executes the Run Command and monitors status
+ */
+class StartBootstrapCommand implements BootstrapCommand {
+
+ private static final long MONITOR_INTERVAL = 5;
+
+ private static final Logger logger = LoggerFactory.getLogger(StartBootstrapCommand.class);
+
+ private final BootstrapCommand runCommand;
+
+ private final BootstrapCommand statusCommand;
+
+ private final ScheduledExecutorService scheduledExecutorService;
+
+ private CommandStatus commandStatus = CommandStatus.ERROR;
+
+ StartBootstrapCommand(final BootstrapCommand runCommand, final BootstrapCommand statusCommand) {
+ this.runCommand = Objects.requireNonNull(runCommand);
+ this.statusCommand = Objects.requireNonNull(statusCommand);
+
+ this.scheduledExecutorService = Executors.newScheduledThreadPool(1, command -> {
+ final Thread thread = new Thread(command);
+ thread.setName(StartBootstrapCommand.class.getSimpleName());
+ return thread;
+ });
+ }
+
+ @Override
+ public CommandStatus getCommandStatus() {
+ return commandStatus;
+ }
+
+ @Override
+ public void run() {
+ runCommand.run();
+ commandStatus = runCommand.getCommandStatus();
+
+ if (CommandStatus.SUCCESS == commandStatus) {
+ logger.info("Application watch started");
+ final WatchCommand watchCommand = new WatchCommand();
+ scheduledExecutorService.scheduleAtFixedRate(watchCommand, MONITOR_INTERVAL, MONITOR_INTERVAL, TimeUnit.SECONDS);
+ commandStatus = CommandStatus.RUNNING;
+ } else {
+ scheduledExecutorService.shutdown();
+ }
+ }
+
+ private class WatchCommand implements Runnable {
+
+ @Override
+ public void run() {
+ statusCommand.run();
+ final CommandStatus status = statusCommand.getCommandStatus();
+ if (CommandStatus.SUCCESS == status) {
+ logger.debug("Application running");
+ } else if (CommandStatus.FAILED == status) {
+ logger.error("Application watch failed");
+ scheduledExecutorService.shutdown();
+ logger.info("Application watch stopped");
+ commandStatus = CommandStatus.FAILED;
+ } else {
+ logger.warn("Application not running [{}]", status);
+ runCommand.run();
+ }
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/StopBootstrapCommand.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/StopBootstrapCommand.java
new file mode 100644
index 000000000000..3e4f47f6c913
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/StopBootstrapCommand.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
+import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Bootstrap Command to run for stopping application
+ */
+class StopBootstrapCommand implements BootstrapCommand {
+
+ private static final Duration FORCE_TERMINATION_TIMEOUT = Duration.ofSeconds(5);
+
+ private static final Logger logger = LoggerFactory.getLogger(ApplicationClassName.BOOTSTRAP_COMMAND.getName());
+
+ private final ProcessHandleProvider processHandleProvider;
+
+ private final ConfigurationProvider configurationProvider;
+
+ private CommandStatus commandStatus = CommandStatus.ERROR;
+
+ StopBootstrapCommand(final ProcessHandleProvider processHandleProvider, final ConfigurationProvider configurationProvider) {
+ this.processHandleProvider = Objects.requireNonNull(processHandleProvider);
+ this.configurationProvider = Objects.requireNonNull(configurationProvider);
+ }
+
+ @Override
+ public CommandStatus getCommandStatus() {
+ return commandStatus;
+ }
+
+ @Override
+ public void run() {
+ final Optional processHandle = processHandleProvider.findApplicationProcessHandle();
+
+ if (processHandle.isEmpty()) {
+ commandStatus = CommandStatus.SUCCESS;
+ logger.info("Application Process not running");
+ } else {
+ stopBootstrapProcess();
+ destroy(processHandle.get());
+ }
+ }
+
+ private void stopBootstrapProcess() {
+ final Optional bootstrapProcessHandleFound = processHandleProvider.findBootstrapProcessHandle();
+ if (bootstrapProcessHandleFound.isPresent()) {
+ final ProcessHandle bootstrapProcessHandle = bootstrapProcessHandleFound.get();
+
+ final boolean destroyRequested = bootstrapProcessHandle.destroy();
+ final long pid = bootstrapProcessHandle.pid();
+ if (destroyRequested) {
+ logger.info("Bootstrap Process [{}] termination requested", pid);
+ onBootstrapDestroyCompleted(bootstrapProcessHandle);
+ } else {
+ logger.warn("Bootstrap Process [{}] termination request failed", pid);
+ }
+ }
+ }
+
+ private void onBootstrapDestroyCompleted(final ProcessHandle bootstrapProcessHandle) {
+ final long pid = bootstrapProcessHandle.pid();
+ final CompletableFuture onExitHandle = bootstrapProcessHandle.onExit();
+ try {
+ final ProcessHandle completedProcessHandle = onExitHandle.get(FORCE_TERMINATION_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
+ logger.info("Bootstrap Process [{}] termination completed", completedProcessHandle.pid());
+ } catch (final Exception e) {
+ logger.warn("Bootstrap Process [{}] termination failed", pid);
+ }
+ }
+
+ private void destroy(final ProcessHandle applicationProcessHandle) {
+ final boolean destroyRequested = applicationProcessHandle.destroy();
+ logger.info("Application Process [{}] termination requested", applicationProcessHandle.pid());
+ if (destroyRequested) {
+ onDestroyCompleted(applicationProcessHandle);
+ } else {
+ logger.warn("Application Process [{}] termination request failed", applicationProcessHandle.pid());
+ destroyForcibly(applicationProcessHandle);
+ }
+ }
+
+ private void destroyForcibly(final ProcessHandle applicationProcessHandle) {
+ final boolean destroyForciblyRequested = applicationProcessHandle.destroyForcibly();
+ if (destroyForciblyRequested) {
+ logger.warn("Application Process [{}] force termination failed", applicationProcessHandle.pid());
+ } else {
+ onDestroyForciblyCompleted(applicationProcessHandle);
+ }
+ }
+
+ private void onDestroyCompleted(final ProcessHandle applicationProcessHandle) {
+ final long pid = applicationProcessHandle.pid();
+ final CompletableFuture onExitHandle = applicationProcessHandle.onExit();
+ final Duration gracefulShutdownTimeout = configurationProvider.getGracefulShutdownTimeout();
+ try {
+ final ProcessHandle completedProcessHandle = onExitHandle.get(gracefulShutdownTimeout.toSeconds(), TimeUnit.SECONDS);
+ logger.info("Application Process [{}] termination completed", completedProcessHandle.pid());
+ commandStatus = CommandStatus.SUCCESS;
+ } catch (final Exception e) {
+ logger.warn("Application Process [{}] termination failed", pid);
+ destroyForcibly(applicationProcessHandle);
+ }
+ }
+
+ private void onDestroyForciblyCompleted(final ProcessHandle applicationProcessHandle) {
+ final long pid = applicationProcessHandle.pid();
+ final CompletableFuture onExitHandle = applicationProcessHandle.onExit();
+ try {
+ final ProcessHandle completedProcessHandle = onExitHandle.get(FORCE_TERMINATION_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
+ logger.warn("Application Process [{}] force termination completed", completedProcessHandle.pid());
+ commandStatus = CommandStatus.SUCCESS;
+ } catch (final Exception e) {
+ logger.warn("Application Process [{}] force termination request failed", pid);
+ commandStatus = CommandStatus.ERROR;
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/UnknownBootstrapCommand.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/UnknownBootstrapCommand.java
new file mode 100644
index 000000000000..a46b03018dbd
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/UnknownBootstrapCommand.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+/**
+ * Bootstrap Command to run for an unknown command requested
+ */
+class UnknownBootstrapCommand implements BootstrapCommand {
+
+ @Override
+ public CommandStatus getCommandStatus() {
+ return CommandStatus.ERROR;
+ }
+
+ @Override
+ public void run() {
+
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/BootstrapArgument.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/BootstrapArgument.java
new file mode 100644
index 000000000000..e1f208661757
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/BootstrapArgument.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.io;
+
+/**
+ * Enumeration of supported arguments for the bootstrap application
+ */
+public enum BootstrapArgument {
+ CLUSTER_STATUS,
+
+ DECOMMISSION,
+
+ DIAGNOSTICS,
+
+ GET_RUN_COMMAND,
+
+ STATUS,
+
+ STATUS_HISTORY,
+
+ START,
+
+ STOP
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/BootstrapArgumentParser.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/BootstrapArgumentParser.java
new file mode 100644
index 000000000000..66500f15da4c
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/BootstrapArgumentParser.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.io;
+
+import java.util.Optional;
+
+/**
+ * Abstraction for parsing application arguments to Bootstrap Arguments
+ */
+public interface BootstrapArgumentParser {
+ /**
+ * Get Bootstrap Argument from application arguments
+ *
+ * @param arguments Application array of arguments
+ * @return Bootstrap Argument or empty when not found
+ */
+ Optional getBootstrapArgument(String[] arguments);
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/FileResponseStreamHandler.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/FileResponseStreamHandler.java
new file mode 100644
index 000000000000..d4127641d72c
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/FileResponseStreamHandler.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.io;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+
+/**
+ * File implementation responsible for reading and transferring responses
+ */
+public class FileResponseStreamHandler implements ResponseStreamHandler {
+ private static final Logger logger = LoggerFactory.getLogger(FileResponseStreamHandler.class);
+
+ private final Path outputPath;
+
+ public FileResponseStreamHandler(final Path outputPath) {
+ this.outputPath = Objects.requireNonNull(outputPath);
+ }
+
+ @Override
+ public void onResponseStream(final InputStream responseStream) {
+ try (OutputStream outputStream = Files.newOutputStream(outputPath)) {
+ responseStream.transferTo(outputStream);
+ } catch (final IOException e) {
+ logger.warn("Write response stream failed for [%s]".formatted(outputPath), e);
+ }
+ }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFiEntryPoint.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/HttpRequestMethod.java
similarity index 81%
rename from nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFiEntryPoint.java
rename to nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/HttpRequestMethod.java
index 386733325e23..e09a5436e67f 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFiEntryPoint.java
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/HttpRequestMethod.java
@@ -14,11 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.nifi;
+package org.apache.nifi.bootstrap.command.io;
-public interface NiFiEntryPoint {
-
- NiFiServer getServer();
+/**
+ * Enumeration of supported HTTP Request Methods for Management Server
+ */
+public enum HttpRequestMethod {
+ DELETE,
- void shutdownHook(boolean isReload);
+ GET
}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/LoggerResponseStreamHandler.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/LoggerResponseStreamHandler.java
new file mode 100644
index 000000000000..b1926cbe5e8d
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/LoggerResponseStreamHandler.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.io;
+
+import org.slf4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Objects;
+
+/**
+ * Logger implementation responsible for reading and logging stream of lines
+ */
+public class LoggerResponseStreamHandler implements ResponseStreamHandler {
+ private final Logger logger;
+
+ public LoggerResponseStreamHandler(final Logger logger) {
+ this.logger = Objects.requireNonNull(logger);
+ }
+
+ @Override
+ public void onResponseStream(final InputStream responseStream) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseStream))) {
+ reader.lines().forEach(logger::info);
+ } catch (final IOException e) {
+ logger.warn("Read response stream failed", e);
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/ResponseStreamHandler.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/ResponseStreamHandler.java
new file mode 100644
index 000000000000..3cbf08dc09e2
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/ResponseStreamHandler.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.io;
+
+import java.io.InputStream;
+
+/**
+ * Response Stream Handler abstraction for reading Management Server responses
+ */
+public interface ResponseStreamHandler {
+ /**
+ * Handle response stream from Management Server
+ *
+ * @param responseStream Response Stream
+ */
+ void onResponseStream(InputStream responseStream);
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/StandardBootstrapArgumentParser.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/StandardBootstrapArgumentParser.java
new file mode 100644
index 000000000000..10c59b301dab
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/io/StandardBootstrapArgumentParser.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.io;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * Standard implementation of Bootstrap Argument Parser supporting enumerated arguments as the first element in an array
+ */
+public class StandardBootstrapArgumentParser implements BootstrapArgumentParser {
+ private static final char HYPHEN = '-';
+
+ private static final char UNDERSCORE = '_';
+
+ /**
+ * Get Bootstrap Argument from first argument provided
+ *
+ * @param arguments Application array of arguments
+ * @return Bootstrap Argument or empty when not found
+ */
+ @Override
+ public Optional getBootstrapArgument(final String[] arguments) {
+ final Optional bootstrapArgumentFound;
+
+ if (arguments == null || arguments.length == 0) {
+ bootstrapArgumentFound = Optional.empty();
+ } else {
+ final String firstArgument = arguments[0];
+ final String formattedArgument = getFormattedArgument(firstArgument);
+ bootstrapArgumentFound = Arrays.stream(BootstrapArgument.values())
+ .filter(bootstrapArgument -> bootstrapArgument.name().equals(formattedArgument))
+ .findFirst();
+ }
+
+ return bootstrapArgumentFound;
+ }
+
+ private String getFormattedArgument(final String argument) {
+ final String upperCased = argument.toUpperCase();
+ return upperCased.replace(HYPHEN, UNDERSCORE);
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ManagementServerAddressProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ManagementServerAddressProvider.java
new file mode 100644
index 000000000000..9585b15a33bc
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ManagementServerAddressProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import java.util.Optional;
+
+/**
+ * Abstraction for producing or locating the Management Server socket address
+ */
+public interface ManagementServerAddressProvider {
+ /**
+ * Get Management Server Address with port number
+ *
+ * @return Management Server Address or empty when not found
+ */
+ Optional getAddress();
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ProcessBuilderProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ProcessBuilderProvider.java
new file mode 100644
index 000000000000..477b7101272a
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ProcessBuilderProvider.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+/**
+ * Abstraction for creating a Process Builder
+ */
+public interface ProcessBuilderProvider {
+ /**
+ * Get Application Process Builder
+ *
+ * @return Process Builder for Application with command arguments configured
+ */
+ ProcessBuilder getApplicationProcessBuilder();
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ProcessHandleManagementServerAddressProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ProcessHandleManagementServerAddressProvider.java
new file mode 100644
index 000000000000..9a40233369fe
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ProcessHandleManagementServerAddressProvider.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import org.apache.nifi.bootstrap.configuration.SystemProperty;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provider implementation resolves the Management Server Address from command arguments of the application Process Handle
+ */
+public class ProcessHandleManagementServerAddressProvider implements ManagementServerAddressProvider {
+ private static final Pattern ADDRESS_ARGUMENT_PATTERN = Pattern.compile("^-D%s=(.+?)$".formatted(SystemProperty.MANAGEMENT_SERVER_ADDRESS.getProperty()));
+
+ private static final int ADDRESS_GROUP = 1;
+
+ private final ProcessHandle processHandle;
+
+ public ProcessHandleManagementServerAddressProvider(final ProcessHandle processHandle) {
+ this.processHandle = Objects.requireNonNull(processHandle);
+ }
+
+ /**
+ * Get Management Server Address with port number from command argument in Process Handle
+ *
+ * @return Management Server Address or null when not found
+ */
+ @Override
+ public Optional getAddress() {
+ final ProcessHandle.Info info = processHandle.info();
+
+ final String managementServerAddress;
+
+ final Optional argumentsFound = info.arguments();
+ if (argumentsFound.isPresent()) {
+ final String[] arguments = argumentsFound.get();
+ managementServerAddress = findManagementServerAddress(arguments);
+ } else {
+ managementServerAddress = null;
+ }
+
+ return Optional.ofNullable(managementServerAddress);
+ }
+
+ private String findManagementServerAddress(final String[] arguments) {
+ String managementServerAddress = null;
+
+ for (final String argument : arguments) {
+ final Matcher matcher = ADDRESS_ARGUMENT_PATTERN.matcher(argument);
+ if (matcher.matches()) {
+ managementServerAddress = matcher.group(ADDRESS_GROUP);
+ break;
+ }
+ }
+
+ return managementServerAddress;
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ProcessHandleProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ProcessHandleProvider.java
new file mode 100644
index 000000000000..e2ba230e7751
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/ProcessHandleProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import java.util.Optional;
+
+/**
+ * Abstraction for finding a Process Handle
+ */
+public interface ProcessHandleProvider {
+ /**
+ * Find Application Process Handle
+ *
+ * @return Application Process Handle or empty when not found
+ */
+ Optional findApplicationProcessHandle();
+
+ /**
+ * Find Bootstrap Process Handle
+ *
+ * @return Bootstrap Process Handle or empty when not found
+ */
+ Optional findBootstrapProcessHandle();
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/StandardManagementServerAddressProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/StandardManagementServerAddressProvider.java
new file mode 100644
index 000000000000..7a13b40e6156
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/StandardManagementServerAddressProvider.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.URI;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.stream.IntStream;
+
+/**
+ * Standard Provider reads optional Server Address from Configuration Provider or selects from available ports
+ */
+public class StandardManagementServerAddressProvider implements ManagementServerAddressProvider {
+ private static final int STANDARD_PORT = 52020;
+
+ private static final int MAXIMUM_PORT = 52050;
+
+ private static final String NOT_AVAILABLE_MESSAGE = "Management Server Port not available in range [%d-%d]".formatted(STANDARD_PORT, MAXIMUM_PORT);
+
+ private static final String LOCALHOST_ADDRESS = "127.0.0.1:%d";
+
+ private static final String HOST_ADDRESS = "%s:%d";
+
+ private final ConfigurationProvider configurationProvider;
+
+ public StandardManagementServerAddressProvider(final ConfigurationProvider configurationProvider) {
+ this.configurationProvider = Objects.requireNonNull(configurationProvider);
+ }
+
+ /**
+ * Get Management Server Address with port number
+ *
+ * @return Management Server Address
+ */
+ @Override
+ public Optional getAddress() {
+ final Optional address;
+
+ final Optional managementServerAddress = configurationProvider.getManagementServerAddress();
+ if (managementServerAddress.isPresent()) {
+ final URI serverAddress = managementServerAddress.get();
+ final String hostAddress = HOST_ADDRESS.formatted(serverAddress.getHost(), serverAddress.getPort());
+ address = Optional.of(hostAddress);
+ } else {
+ final int serverPort = getServerPort();
+ address = Optional.of(LOCALHOST_ADDRESS.formatted(serverPort));
+ }
+
+ return address;
+ }
+
+ private int getServerPort() {
+ final OptionalInt portFound = IntStream.range(STANDARD_PORT, MAXIMUM_PORT)
+ .filter(StandardManagementServerAddressProvider::isPortFree)
+ .findFirst();
+
+ return portFound.orElseThrow(() -> new IllegalStateException(NOT_AVAILABLE_MESSAGE));
+ }
+
+ private static boolean isPortFree(final int port) {
+ try (ServerSocket ignored = new ServerSocket(port)) {
+ return true;
+ } catch (final IOException e) {
+ return false;
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/StandardProcessBuilderProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/StandardProcessBuilderProvider.java
new file mode 100644
index 000000000000..f7803bf904cd
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/StandardProcessBuilderProvider.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.apache.nifi.bootstrap.configuration.SystemProperty;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.BiPredicate;
+import java.util.stream.Stream;
+
+/**
+ * Standard implementation of Process Builder Provider for constructing application command arguments
+ */
+public class StandardProcessBuilderProvider implements ProcessBuilderProvider {
+ private static final String JAR_FILE_EXTENSION = ".jar";
+
+ private static final BiPredicate JAR_FILE_MATCHER = (path, attributes) -> path.getFileName().toString().endsWith(JAR_FILE_EXTENSION);
+
+ private static final int LIBRARY_JAR_DEPTH = 1;
+
+ private static final String SYSTEM_PROPERTY = "-D%s=%s";
+
+ private static final String CLASS_PATH_ARGUMENT = "--class-path";
+
+ private final ConfigurationProvider configurationProvider;
+
+ private final ManagementServerAddressProvider managementServerAddressProvider;
+
+ public StandardProcessBuilderProvider(final ConfigurationProvider configurationProvider, final ManagementServerAddressProvider managementServerAddressProvider) {
+ this.configurationProvider = Objects.requireNonNull(configurationProvider);
+ this.managementServerAddressProvider = Objects.requireNonNull(managementServerAddressProvider);
+ }
+
+ @Override
+ public ProcessBuilder getApplicationProcessBuilder() {
+ final ProcessBuilder processBuilder = new ProcessBuilder();
+
+ final List command = getCommand();
+ processBuilder.command(command);
+
+ return processBuilder;
+ }
+
+ private List getCommand() {
+ final List command = new ArrayList<>();
+
+ final ProcessHandle.Info currentProcessHandleInfo = ProcessHandle.current().info();
+ final String currentProcessCommand = getCurrentProcessCommand(currentProcessHandleInfo);
+ command.add(currentProcessCommand);
+
+ final String classPath = getClassPath();
+ command.add(CLASS_PATH_ARGUMENT);
+ command.add(classPath);
+
+ final Path logDirectory = configurationProvider.getLogDirectory();
+ final String logDirectoryProperty = SYSTEM_PROPERTY.formatted(SystemProperty.LOG_DIRECTORY.getProperty(), logDirectory);
+ command.add(logDirectoryProperty);
+
+ final Path applicationProperties = configurationProvider.getApplicationProperties();
+ final String applicationPropertiesProperty = SYSTEM_PROPERTY.formatted(SystemProperty.APPLICATION_PROPERTIES.getProperty(), applicationProperties);
+ command.add(applicationPropertiesProperty);
+
+ final String managementServerAddress = managementServerAddressProvider.getAddress().orElseThrow(() -> new IllegalStateException("Management Server Address not configured"));
+ final String managementServerAddressProperty = SYSTEM_PROPERTY.formatted(SystemProperty.MANAGEMENT_SERVER_ADDRESS.getProperty(), managementServerAddress);
+ command.add(managementServerAddressProperty);
+
+ final List additionalArguments = configurationProvider.getAdditionalArguments();
+ command.addAll(additionalArguments);
+
+ command.add(ApplicationClassName.APPLICATION.getName());
+ return command;
+ }
+
+ private String getCurrentProcessCommand(final ProcessHandle.Info currentProcessHandleInfo) {
+ final Optional currentProcessHandleCommand = currentProcessHandleInfo.command();
+ return currentProcessHandleCommand.orElseThrow(IllegalStateException::new);
+ }
+
+ private String getClassPath() {
+ final Path libraryDirectory = configurationProvider.getLibraryDirectory();
+ try (
+ Stream libraryFiles = Files.find(libraryDirectory, LIBRARY_JAR_DEPTH, JAR_FILE_MATCHER)
+ ) {
+ final List libraryPaths = new ArrayList<>(libraryFiles.map(Path::toString).toList());
+
+ final Path configurationDirectory = configurationProvider.getConfigurationDirectory();
+ libraryPaths.add(configurationDirectory.toString());
+
+ return String.join(File.pathSeparator, libraryPaths);
+ } catch (final IOException e) {
+ throw new IllegalStateException("Read Library Directory [%s] failed".formatted(libraryDirectory), e);
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/StandardProcessHandleProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/StandardProcessHandleProvider.java
new file mode 100644
index 000000000000..f0f7cd14539e
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/command/process/StandardProcessHandleProvider.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.apache.nifi.bootstrap.configuration.SystemProperty;
+
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+/**
+ * Process Handle Provider searches running Processes and locates the first handles match based on command arguments
+ */
+public class StandardProcessHandleProvider implements ProcessHandleProvider {
+
+ private static final String PROPERTIES_ARGUMENT = "-D%s=%s";
+
+ private final ConfigurationProvider configurationProvider;
+
+ public StandardProcessHandleProvider(final ConfigurationProvider configurationProvider) {
+ this.configurationProvider = Objects.requireNonNull(configurationProvider);
+ }
+
+ /**
+ * Find Process Handle for Application based on matching argument for path to application properties
+ *
+ * @return Application Process Handle or empty when not found
+ */
+ @Override
+ public Optional findApplicationProcessHandle() {
+ final Path applicationProperties = configurationProvider.getApplicationProperties();
+ return findProcessHandle(SystemProperty.APPLICATION_PROPERTIES, applicationProperties);
+ }
+
+ /**
+ * Find Process Handle for Bootstrap based on matching argument for path to bootstrap configuration
+ *
+ * @return Bootstrap Process Handle or empty when not found
+ */
+ @Override
+ public Optional findBootstrapProcessHandle() {
+ final Path bootstrapConfiguration = configurationProvider.getBootstrapConfiguration();
+ return findProcessHandle(SystemProperty.BOOTSTRAP_CONFIGURATION, bootstrapConfiguration);
+ }
+
+ private Optional findProcessHandle(final SystemProperty systemProperty, final Path configuration) {
+ final String propertiesArgument = PROPERTIES_ARGUMENT.formatted(systemProperty.getProperty(), configuration);
+ final ProcessHandle currentProcessHandle = ProcessHandle.current();
+
+ return ProcessHandle.allProcesses()
+ .filter(Predicate.not(currentProcessHandle::equals))
+ .filter(processHandle -> {
+ final ProcessHandle.Info processHandleInfo = processHandle.info();
+ final Optional processArguments = processHandleInfo.arguments();
+ final boolean matched;
+ if (processArguments.isPresent()) {
+ final String[] arguments = processArguments.get();
+ matched = Arrays.stream(arguments).anyMatch(propertiesArgument::contentEquals);
+ } else {
+ matched = false;
+ }
+ return matched;
+ })
+ .findFirst();
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/ApplicationClassName.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/ApplicationClassName.java
new file mode 100644
index 000000000000..154e6ce9a056
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/ApplicationClassName.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.configuration;
+
+/**
+ * Enumeration of application class names for processes and logging
+ */
+public enum ApplicationClassName {
+ APPLICATION("org.apache.nifi.NiFi"),
+
+ BOOTSTRAP_COMMAND("org.apache.nifi.bootstrap.Command");
+
+ private final String name;
+
+ ApplicationClassName(final String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/BootstrapProperty.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/BootstrapProperty.java
new file mode 100644
index 000000000000..571cc04cc298
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/BootstrapProperty.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.configuration;
+
+/**
+ * Enumeration of supported bootstrap properties for the application
+ */
+public enum BootstrapProperty {
+ CONFIGURATION_DIRECTORY("conf.dir"),
+
+ GRACEFUL_SHUTDOWN_SECONDS("graceful.shutdown.seconds"),
+
+ JAVA_ARGUMENT("java.arg"),
+
+ LIBRARY_DIRECTORY("lib.dir"),
+
+ MANAGEMENT_SERVER_ADDRESS("management.server.address"),
+
+ WORKING_DIRECTORY("working.dir");
+
+ private final String property;
+
+ BootstrapProperty(final String property) {
+ this.property = property;
+ }
+
+ public String getProperty() {
+ return property;
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/ConfigurationProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/ConfigurationProvider.java
new file mode 100644
index 000000000000..2b6d95c38f87
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/ConfigurationProvider.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.configuration;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Abstraction for access to application configuration properties
+ */
+public interface ConfigurationProvider {
+ /**
+ * Get additional arguments for application command
+ *
+ * @return Additional arguments
+ */
+ List getAdditionalArguments();
+
+ /**
+ * Get file containing application properties
+ *
+ * @return Application properties
+ */
+ Path getApplicationProperties();
+
+ /**
+ * Get file containing bootstrap configuration
+ *
+ * @return Bootstrap configuration
+ */
+ Path getBootstrapConfiguration();
+
+ /**
+ * Get directory containing application configuration
+ *
+ * @return Configuration directory
+ */
+ Path getConfigurationDirectory();
+
+ /**
+ * Get directory containing application libraries
+ *
+ * @return Library directory
+ */
+ Path getLibraryDirectory();
+
+ /**
+ * Get directory containing logs
+ *
+ * @return Log directory
+ */
+ Path getLogDirectory();
+
+ /**
+ * Get timeout configured for graceful shutdown of application process
+ *
+ * @return Graceful Shutdown Timeout duration
+ */
+ Duration getGracefulShutdownTimeout();
+
+ /**
+ * Get Management Server Address from the bootstrap configuration
+ *
+ * @return Management Server Address or empty when not configured
+ */
+ Optional getManagementServerAddress();
+
+ /**
+ * Get directory for current operations and resolving relative paths
+ *
+ * @return Working directory
+ */
+ Path getWorkingDirectory();
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/EnvironmentVariable.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/EnvironmentVariable.java
new file mode 100644
index 000000000000..940d53571000
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/EnvironmentVariable.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.configuration;
+
+/**
+ * Enumeration of supported bootstrap and application environment variables
+ */
+public enum EnvironmentVariable {
+ NIFI_HOME
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/InvalidCommandException.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/ManagementServerPath.java
similarity index 63%
rename from nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/InvalidCommandException.java
rename to nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/ManagementServerPath.java
index 36873e7a8a4e..3a3ff050ad35 100644
--- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/InvalidCommandException.java
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/ManagementServerPath.java
@@ -14,25 +14,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.nifi.bootstrap;
+package org.apache.nifi.bootstrap.configuration;
-public class InvalidCommandException extends Exception {
+/**
+ * Enumeration of Management Server HTTP resource paths
+ */
+public enum ManagementServerPath {
+ HEALTH("/health"),
- private static final long serialVersionUID = 1L;
+ HEALTH_CLUSTER("/health/cluster"),
- public InvalidCommandException() {
- super();
- }
+ HEALTH_DIAGNOSTICS("/health/diagnostics"),
- public InvalidCommandException(final String message) {
- super(message);
- }
+ HEALTH_STATUS_HISTORY("/health/status-history");
+
+ private final String path;
- public InvalidCommandException(final Throwable t) {
- super(t);
+ ManagementServerPath(final String path) {
+ this.path = path;
}
- public InvalidCommandException(final String message, final Throwable t) {
- super(message, t);
+ public String getPath() {
+ return path;
}
}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/StandardConfigurationProvider.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/StandardConfigurationProvider.java
new file mode 100644
index 000000000000..1901bfde935e
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/StandardConfigurationProvider.java
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.configuration;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Properties;
+
+/**
+ * Standard implementation of Configuration Provider based on NIFI_HOME environment variable base directory
+ */
+public class StandardConfigurationProvider implements ConfigurationProvider {
+
+ private static final String CONFIGURATION_DIRECTORY = "conf";
+
+ private static final String LIBRARY_DIRECTORY = "lib";
+
+ private static final String LOG_DIRECTORY = "logs";
+
+ private static final String APPLICATION_PROPERTIES = "nifi.properties";
+
+ private static final String BOOTSTRAP_CONFIGURATION = "bootstrap.conf";
+
+ private static final String CURRENT_DIRECTORY = "";
+
+ private static final Duration GRACEFUL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(20);
+
+ private final Map environmentVariables;
+
+ private final Properties systemProperties;
+
+ private final Properties bootstrapProperties = new Properties();
+
+ public StandardConfigurationProvider(final Map environmentVariables, final Properties systemProperties) {
+ this.environmentVariables = Objects.requireNonNull(environmentVariables);
+ this.systemProperties = Objects.requireNonNull(systemProperties);
+ setBootstrapProperties();
+ }
+
+ /**
+ * Get additional arguments for application command from Bootstrap Properties starting with java.arg
+ *
+ * @return Additional arguments
+ */
+ @Override
+ public List getAdditionalArguments() {
+ final List additionalArguments = new ArrayList<>();
+
+ for (final String propertyName : bootstrapProperties.stringPropertyNames()) {
+ if (propertyName.startsWith(BootstrapProperty.JAVA_ARGUMENT.getProperty())) {
+ final String additionalArgument = bootstrapProperties.getProperty(propertyName);
+ if (!additionalArgument.isBlank()) {
+ additionalArguments.add(additionalArgument);
+ }
+ }
+ }
+
+ return additionalArguments;
+ }
+
+ /**
+ * Get Application Properties relative to configuration directory
+ *
+ * @return Application Properties
+ */
+ @Override
+ public Path getApplicationProperties() {
+ final Path configurationDirectory = getConfigurationDirectory();
+ final Path applicationProperties = configurationDirectory.resolve(APPLICATION_PROPERTIES);
+
+ if (Files.notExists(applicationProperties)) {
+ throw new IllegalStateException("Application Properties [%s] not found".formatted(applicationProperties));
+ }
+
+ return applicationProperties;
+ }
+
+ /**
+ * Get Bootstrap Configuration from either System Property or relative to configuration directory
+ *
+ * @return Bootstrap Configuration
+ */
+ @Override
+ public Path getBootstrapConfiguration() {
+ final Path bootstrapConfiguration;
+
+ final String bootstrapConfigurationProperty = System.getProperty(SystemProperty.BOOTSTRAP_CONFIGURATION.getProperty());
+ if (isEmpty(bootstrapConfigurationProperty)) {
+ final Path configurationDirectory = getConfigurationDirectory();
+ bootstrapConfiguration = configurationDirectory.resolve(BOOTSTRAP_CONFIGURATION);
+ } else {
+ bootstrapConfiguration = Paths.get(bootstrapConfigurationProperty).toAbsolutePath();
+ }
+
+ if (Files.notExists(bootstrapConfiguration)) {
+ throw new IllegalStateException("Bootstrap Configuration [%s] not found".formatted(bootstrapConfiguration));
+ }
+
+ return bootstrapConfiguration;
+ }
+
+ /**
+ * Get Library Directory from Bootstrap Configuration or relative to configuration directory
+ *
+ * @return Library Directory
+ */
+ @Override
+ public Path getLibraryDirectory() {
+ final Path libraryDirectory = getResolvedDirectory(BootstrapProperty.LIBRARY_DIRECTORY, LIBRARY_DIRECTORY);
+
+ if (Files.notExists(libraryDirectory)) {
+ throw new IllegalStateException("Library Directory [%s] not found".formatted(libraryDirectory));
+ }
+
+ return libraryDirectory;
+ }
+
+ /**
+ * Get Log Directory from System Property or relative to application home directory
+ *
+ * @return Log Directory
+ */
+ @Override
+ public Path getLogDirectory() {
+ final Path logDirectory;
+
+ final String logDirectoryProperty = systemProperties.getProperty(SystemProperty.LOG_DIRECTORY.getProperty());
+
+ if (isEmpty(logDirectoryProperty)) {
+ final Path applicationHome = getApplicationHome();
+ logDirectory = applicationHome.resolve(LOG_DIRECTORY);
+ } else {
+ logDirectory = Paths.get(logDirectoryProperty);
+ }
+
+ return logDirectory;
+ }
+
+ /**
+ * Get timeout configured for graceful shutdown of application process
+ *
+ * @return Graceful Shutdown Timeout duration
+ */
+ @Override
+ public Duration getGracefulShutdownTimeout() {
+ final Duration gracefulShutdownTimeout;
+
+ final String gracefulShutdownSecondsProperty = bootstrapProperties.getProperty(BootstrapProperty.GRACEFUL_SHUTDOWN_SECONDS.getProperty());
+ if (gracefulShutdownSecondsProperty == null || gracefulShutdownSecondsProperty.isEmpty()) {
+ gracefulShutdownTimeout = GRACEFUL_SHUTDOWN_TIMEOUT;
+ } else {
+ final int gracefulShutdownSeconds = Integer.parseInt(gracefulShutdownSecondsProperty);
+ gracefulShutdownTimeout = Duration.ofSeconds(gracefulShutdownSeconds);
+ }
+
+ return gracefulShutdownTimeout;
+ }
+
+ /**
+ * Get Management Server Address from the bootstrap configuration
+ *
+ * @return Management Server Address or empty when not configured
+ */
+ @Override
+ public Optional getManagementServerAddress() {
+ final Optional managementServerAddress;
+
+ final String managementServerAddressProperty = bootstrapProperties.getProperty(BootstrapProperty.MANAGEMENT_SERVER_ADDRESS.getProperty());
+ if (managementServerAddressProperty == null || managementServerAddressProperty.isEmpty()) {
+ managementServerAddress = Optional.empty();
+ } else {
+ final URI serverAddress = URI.create(managementServerAddressProperty);
+ managementServerAddress = Optional.of(serverAddress);
+ }
+
+ return managementServerAddress;
+ }
+
+ /**
+ * Get Configuration Directory from Bootstrap Configuration or relative to application home directory
+ *
+ * @return Configuration Directory
+ */
+ @Override
+ public Path getConfigurationDirectory() {
+ final Path configurationDirectory = getResolvedDirectory(BootstrapProperty.CONFIGURATION_DIRECTORY, CONFIGURATION_DIRECTORY);
+
+ if (Files.notExists(configurationDirectory)) {
+ throw new IllegalStateException("Configuration Directory [%s] not found".formatted(configurationDirectory));
+ }
+
+ return configurationDirectory;
+ }
+
+ /**
+ * Get Working Directory from Bootstrap Configuration or current working directory
+ *
+ * @return Working Directory
+ */
+ @Override
+ public Path getWorkingDirectory() {
+ final Path workingDirectory;
+
+ final String workingDirectoryProperty = bootstrapProperties.getProperty(BootstrapProperty.WORKING_DIRECTORY.getProperty());
+ if (isEmpty(workingDirectoryProperty)) {
+ workingDirectory = Paths.get(CURRENT_DIRECTORY).toAbsolutePath();
+ } else {
+ workingDirectory = Paths.get(workingDirectoryProperty).toAbsolutePath();
+ }
+
+ return workingDirectory;
+ }
+
+ private Path getResolvedDirectory(final BootstrapProperty bootstrapProperty, final String relativeDirectory) {
+ final Path resolvedDirectory;
+
+ final String directoryProperty = bootstrapProperties.getProperty(bootstrapProperty.getProperty());
+ if (isEmpty(directoryProperty)) {
+ final Path applicationHome = getApplicationHome();
+ resolvedDirectory = applicationHome.resolve(relativeDirectory);
+ } else {
+ final Path directoryPropertyResolved = Paths.get(directoryProperty);
+ if (directoryPropertyResolved.isAbsolute()) {
+ resolvedDirectory = directoryPropertyResolved;
+ } else {
+ final Path workingDirectory = getWorkingDirectory();
+ resolvedDirectory = workingDirectory.resolve(directoryPropertyResolved);
+ }
+ }
+
+ // Normalize Path removing relative directory elements
+ return resolvedDirectory.normalize();
+ }
+
+ private Path getApplicationHome() {
+ final Path applicationHome;
+
+ final String applicationHomeVariable = environmentVariables.get(EnvironmentVariable.NIFI_HOME.name());
+ if (isEmpty(applicationHomeVariable)) {
+ throw new IllegalStateException("Application Home Environment Variable [NIFI_HOME] not configured");
+ } else {
+ applicationHome = Paths.get(applicationHomeVariable).toAbsolutePath();
+ }
+
+ if (Files.notExists(applicationHome)) {
+ throw new IllegalStateException("Application Home [%s] not found".formatted(applicationHome));
+ }
+
+ return applicationHome;
+ }
+
+ private boolean isEmpty(final String property) {
+ return property == null || property.isEmpty();
+ }
+
+ private void setBootstrapProperties() {
+ final Path bootstrapConfiguration = getBootstrapConfiguration();
+
+ try (InputStream inputStream = Files.newInputStream(bootstrapConfiguration)) {
+ bootstrapProperties.load(inputStream);
+ } catch (final IOException e) {
+ throw new UncheckedIOException("Bootstrap Properties [%s] loading failed".formatted(bootstrapConfiguration), e);
+ }
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/SystemProperty.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/SystemProperty.java
new file mode 100644
index 000000000000..54daa12b664e
--- /dev/null
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/configuration/SystemProperty.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.configuration;
+
+/**
+ * Enumeration of supported system properties for the application
+ */
+public enum SystemProperty {
+ /** Path to application properties file */
+ APPLICATION_PROPERTIES("nifi.properties.file.path"),
+
+ /** Path to bootstrap configuration file */
+ BOOTSTRAP_CONFIGURATION("org.apache.nifi.bootstrap.config.file"),
+
+ /** Path to log directory */
+ LOG_DIRECTORY("org.apache.nifi.bootstrap.config.log.dir"),
+
+ /** Socket address and port number for management server */
+ MANAGEMENT_SERVER_ADDRESS("org.apache.nifi.management.server.address");
+
+ private final String property;
+
+ SystemProperty(final String property) {
+ this.property = property;
+ }
+
+ public String getProperty() {
+ return property;
+ }
+}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/DumpFileValidator.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/DumpFileValidator.java
deleted file mode 100644
index 8eaef041e715..000000000000
--- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/DumpFileValidator.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.bootstrap.util;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.Closeable;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.InvalidPathException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-public final class DumpFileValidator {
-
- private static final Logger logger = LoggerFactory.getLogger(DumpFileValidator.class);
-
- private DumpFileValidator() {
- }
-
- public static boolean validate(final String filePath) {
- try {
- final Path path = Paths.get(filePath);
- return checkFileCanBeCreated(path);
- } catch (InvalidPathException e) {
- System.err.println("Invalid filename. The command parameters are: status-history ");
- return false;
- }
- }
-
- private static boolean checkFileCanBeCreated(final Path path) {
- try (final FileOutputStream outputStream = new FileOutputStream(path.toString());
- final Closeable onClose = () -> Files.delete(path)) {
- } catch (FileNotFoundException e) {
- System.err.println("Invalid filename or there's no write permission to the currently selected file path.");
- return false;
- } catch (IOException e) {
- logger.error("Could not delete file while validating file path.");
- }
- return true;
- }
-}
diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/LimitingInputStream.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/LimitingInputStream.java
deleted file mode 100644
index 214934222cce..000000000000
--- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/LimitingInputStream.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.bootstrap.util;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public class LimitingInputStream extends InputStream {
-
- private final InputStream in;
- private final long limit;
- private long bytesRead = 0;
-
- public LimitingInputStream(final InputStream in, final long limit) {
- this.in = in;
- this.limit = limit;
- }
-
- @Override
- public int read() throws IOException {
- if (bytesRead >= limit) {
- return -1;
- }
-
- final int val = in.read();
- if (val > -1) {
- bytesRead++;
- }
- return val;
- }
-
- @Override
- public int read(final byte[] b) throws IOException {
- if (bytesRead >= limit) {
- return -1;
- }
-
- final int maxToRead = (int) Math.min(b.length, limit - bytesRead);
-
- final int val = in.read(b, 0, maxToRead);
- if (val > 0) {
- bytesRead += val;
- }
- return val;
- }
-
- @Override
- public int read(byte[] b, int off, int len) throws IOException {
- if (bytesRead >= limit) {
- return -1;
- }
-
- final int maxToRead = (int) Math.min(len, limit - bytesRead);
-
- final int val = in.read(b, off, maxToRead);
- if (val > 0) {
- bytesRead += val;
- }
- return val;
- }
-
- @Override
- public long skip(final long n) throws IOException {
- final long skipped = in.skip(Math.min(n, limit - bytesRead));
- bytesRead += skipped;
- return skipped;
- }
-
- @Override
- public int available() throws IOException {
- return in.available();
- }
-
- @Override
- public void close() throws IOException {
- in.close();
- }
-
- @Override
- public void mark(int readlimit) {
- in.mark(readlimit);
- }
-
- @Override
- public boolean markSupported() {
- return in.markSupported();
- }
-
- @Override
- public void reset() throws IOException {
- in.reset();
- }
-}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/ApplicationProcessStatusBootstrapCommandTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/ApplicationProcessStatusBootstrapCommandTest.java
new file mode 100644
index 000000000000..f83cf51007b7
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/ApplicationProcessStatusBootstrapCommandTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ApplicationProcessStatusBootstrapCommandTest {
+ @Mock
+ private ProcessHandle processHandle;
+
+ @Mock
+ private ProcessHandle applicationProcessHandle;
+
+ private ApplicationProcessStatusBootstrapCommand command;
+
+ @BeforeEach
+ void setCommand() {
+ command = new ApplicationProcessStatusBootstrapCommand(processHandle);
+ }
+
+ @Test
+ void testRunStopped() {
+ when(processHandle.children()).thenReturn(Stream.empty());
+
+ command.run();
+ final CommandStatus commandStatus = command.getCommandStatus();
+
+ assertEquals(CommandStatus.STOPPED, commandStatus);
+ }
+
+ @Test
+ void testRunSuccess() {
+ when(processHandle.children()).thenReturn(Stream.of(applicationProcessHandle));
+ when(applicationProcessHandle.isAlive()).thenReturn(true);
+
+ command.run();
+ final CommandStatus commandStatus = command.getCommandStatus();
+
+ assertEquals(CommandStatus.SUCCESS, commandStatus);
+ }
+
+ @Test
+ void testRunCommunicationFailed() {
+ when(processHandle.children()).thenReturn(Stream.of(applicationProcessHandle));
+ when(applicationProcessHandle.isAlive()).thenReturn(false);
+
+ command.run();
+ final CommandStatus commandStatus = command.getCommandStatus();
+
+ assertEquals(CommandStatus.COMMUNICATION_FAILED, commandStatus);
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/GetRunCommandBootstrapCommandTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/GetRunCommandBootstrapCommandTest.java
new file mode 100644
index 000000000000..4e9cc2960e21
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/GetRunCommandBootstrapCommandTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
+import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class GetRunCommandBootstrapCommandTest {
+ private static final String CONFIGURATION_DIRECTORY = "conf";
+
+ private static final String SPACE_SEPARATOR = " ";
+
+ @Mock
+ private ConfigurationProvider configurationProvider;
+
+ @Mock
+ private ProcessHandleProvider processHandleProvider;
+
+ @Test
+ void testRun(@TempDir final Path workingDirectory) throws IOException {
+ final Path configurationDirectory = workingDirectory.resolve(CONFIGURATION_DIRECTORY);
+ assertTrue(configurationDirectory.toFile().mkdir());
+
+ final Path applicationProperties = configurationDirectory.resolve(Properties.class.getSimpleName());
+ Files.writeString(applicationProperties, SPACE_SEPARATOR);
+
+ when(configurationProvider.getApplicationProperties()).thenReturn(applicationProperties);
+ when(configurationProvider.getLibraryDirectory()).thenReturn(workingDirectory);
+ when(configurationProvider.getConfigurationDirectory()).thenReturn(configurationDirectory);
+
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final PrintStream printStream = new PrintStream(outputStream);
+
+ final GetRunCommandBootstrapCommand command = new GetRunCommandBootstrapCommand(configurationProvider, processHandleProvider, printStream);
+ command.run();
+
+ final CommandStatus commandStatus = command.getCommandStatus();
+
+ assertNotNull(commandStatus);
+ assertEquals(CommandStatus.SUCCESS, commandStatus);
+
+ final String runCommand = outputStream.toString().trim();
+ final List runCommands = List.of(runCommand.split(SPACE_SEPARATOR));
+
+ final String lastCommand = runCommands.getLast();
+ assertEquals(ApplicationClassName.APPLICATION.getName(), lastCommand);
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/RunBootstrapCommandTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/RunBootstrapCommandTest.java
new file mode 100644
index 000000000000..a8870c23616c
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/RunBootstrapCommandTest.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@ExtendWith(MockitoExtension.class)
+class RunBootstrapCommandTest {
+ @Mock
+ private ConfigurationProvider configurationProvider;
+
+ @Mock
+ private ProcessHandleProvider processHandleProvider;
+
+ private RunBootstrapCommand command;
+
+ @BeforeEach
+ void setCommand() {
+ command = new RunBootstrapCommand(configurationProvider, processHandleProvider);
+ }
+
+ @Test
+ void testRun() {
+ command.run();
+
+ final CommandStatus commandStatus = command.getCommandStatus();
+
+ assertNotNull(commandStatus);
+ assertEquals(CommandStatus.FAILED, commandStatus);
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/StandardBootstrapCommandProviderTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/StandardBootstrapCommandProviderTest.java
new file mode 100644
index 000000000000..53a3377e888b
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/StandardBootstrapCommandProviderTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class StandardBootstrapCommandProviderTest {
+ private StandardBootstrapCommandProvider provider;
+
+ @BeforeEach
+ void setProvider() {
+ provider = new StandardBootstrapCommandProvider();
+ }
+
+ @Test
+ void testGetBootstrapCommandNull() {
+ final BootstrapCommand bootstrapCommand = provider.getBootstrapCommand(null);
+
+ assertNotNull(bootstrapCommand);
+ assertInstanceOf(UnknownBootstrapCommand.class, bootstrapCommand);
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/StartBootstrapCommandTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/StartBootstrapCommandTest.java
new file mode 100644
index 000000000000..2d9b0a642fd1
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/StartBootstrapCommandTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class StartBootstrapCommandTest {
+ @Mock
+ private BootstrapCommand runBootstrapCommand;
+
+ @Mock
+ private BootstrapCommand statusBootstrapCommand;
+
+ private StartBootstrapCommand command;
+
+ @BeforeEach
+ void setCommand() {
+ command = new StartBootstrapCommand(runBootstrapCommand, statusBootstrapCommand);
+ }
+
+ @Test
+ void testRunError() {
+ final CommandStatus runCommandStatus = CommandStatus.ERROR;
+ when(runBootstrapCommand.getCommandStatus()).thenReturn(runCommandStatus);
+
+ command.run();
+
+ final CommandStatus commandStatus = command.getCommandStatus();
+ assertEquals(runCommandStatus, commandStatus);
+ }
+
+ @Test
+ void testRunSuccessFailed() {
+ final CommandStatus runCommandStatus = CommandStatus.SUCCESS;
+ when(runBootstrapCommand.getCommandStatus()).thenReturn(runCommandStatus);
+
+ final CommandStatus statusCommandStatus = CommandStatus.FAILED;
+ lenient().when(statusBootstrapCommand.getCommandStatus()).thenReturn(statusCommandStatus);
+
+ command.run();
+
+ final CommandStatus commandStatus = command.getCommandStatus();
+ assertEquals(CommandStatus.RUNNING, commandStatus);
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/StopBootstrapCommandTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/StopBootstrapCommandTest.java
new file mode 100644
index 000000000000..37f293d933cd
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/StopBootstrapCommandTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command;
+
+import org.apache.nifi.bootstrap.command.process.ProcessHandleProvider;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class StopBootstrapCommandTest {
+ @Mock
+ private ConfigurationProvider configurationProvider;
+
+ @Mock
+ private ProcessHandleProvider processHandleProvider;
+
+ @Mock
+ private ProcessHandle applicationProcessHandle;
+
+ @Mock
+ private ProcessHandle.Info applicationProcessHandleInfo;
+
+ private StopBootstrapCommand command;
+
+ @BeforeEach
+ void setCommand() {
+ command = new StopBootstrapCommand(processHandleProvider, configurationProvider);
+ }
+
+ @Test
+ void testRunProcessHandleNotFound() {
+ when(processHandleProvider.findApplicationProcessHandle()).thenReturn(Optional.empty());
+
+ command.run();
+ final CommandStatus commandStatus = command.getCommandStatus();
+
+ assertEquals(CommandStatus.SUCCESS, commandStatus);
+ }
+
+ @Test
+ void testRunDestroyCompleted() {
+ when(processHandleProvider.findApplicationProcessHandle()).thenReturn(Optional.of(applicationProcessHandle));
+ when(applicationProcessHandle.destroy()).thenReturn(true);
+ when(applicationProcessHandle.onExit()).thenReturn(CompletableFuture.completedFuture(applicationProcessHandle));
+
+ command.run();
+ final CommandStatus commandStatus = command.getCommandStatus();
+
+ assertEquals(CommandStatus.SUCCESS, commandStatus);
+ }
+
+ @Test
+ void testRunDestroyFailed() {
+ when(processHandleProvider.findApplicationProcessHandle()).thenReturn(Optional.of(applicationProcessHandle));
+ when(applicationProcessHandle.destroy()).thenReturn(true);
+ when(applicationProcessHandle.onExit()).thenReturn(CompletableFuture.failedFuture(new RuntimeException()));
+
+ command.run();
+ final CommandStatus commandStatus = command.getCommandStatus();
+
+ assertEquals(CommandStatus.ERROR, commandStatus);
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/io/FileResponseStreamHandlerTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/io/FileResponseStreamHandlerTest.java
new file mode 100644
index 000000000000..c8fbef5878dd
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/io/FileResponseStreamHandlerTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.io;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+class FileResponseStreamHandlerTest {
+
+ @Test
+ void testOnResponseStream(@TempDir final Path outputDirectory) throws IOException {
+ final Path outputPath = outputDirectory.resolve(FileResponseStreamHandlerTest.class.getSimpleName());
+
+ final FileResponseStreamHandler handler = new FileResponseStreamHandler(outputPath);
+
+ final byte[] bytes = String.class.getName().getBytes(StandardCharsets.UTF_8);
+ final InputStream inputStream = new ByteArrayInputStream(bytes);
+
+ handler.onResponseStream(inputStream);
+
+ final byte[] read = Files.readAllBytes(outputPath);
+ assertArrayEquals(bytes, read);
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/io/StandardBootstrapArgumentParserTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/io/StandardBootstrapArgumentParserTest.java
new file mode 100644
index 000000000000..7201309589c3
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/io/StandardBootstrapArgumentParserTest.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.io;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StandardBootstrapArgumentParserTest {
+ private static final String CLUSTER_STATUS_ARGUMENT = "cluster-status";
+
+ private StandardBootstrapArgumentParser parser;
+
+ @BeforeEach
+ void setParser() {
+ parser = new StandardBootstrapArgumentParser();
+ }
+
+ @Test
+ void testGetBootstrapArgumentNull() {
+ final Optional bootstrapArgumentFound = parser.getBootstrapArgument(null);
+
+ assertTrue(bootstrapArgumentFound.isEmpty());
+ }
+
+ @Test
+ void testGetBootstrapArgumentClusterStatus() {
+ final Optional bootstrapArgumentFound = parser.getBootstrapArgument(new String[]{CLUSTER_STATUS_ARGUMENT});
+
+ assertTrue(bootstrapArgumentFound.isPresent());
+ assertEquals(BootstrapArgument.CLUSTER_STATUS, bootstrapArgumentFound.get());
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/ProcessHandleManagementServerAddressProviderTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/ProcessHandleManagementServerAddressProviderTest.java
new file mode 100644
index 000000000000..6103349d83b2
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/ProcessHandleManagementServerAddressProviderTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import org.apache.nifi.bootstrap.configuration.SystemProperty;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ProcessHandleManagementServerAddressProviderTest {
+ private static final String SYSTEM_PROPERTY_ARGUMENT = "-D%s=%s";
+
+ private static final String ADDRESS = "127.0.0.1:52020";
+
+ @Mock
+ private ProcessHandle processHandle;
+
+ @Mock
+ private ProcessHandle.Info processHandleInfo;
+
+ private ProcessHandleManagementServerAddressProvider provider;
+
+ @BeforeEach
+ void setProvider() {
+ provider = new ProcessHandleManagementServerAddressProvider(processHandle);
+ }
+
+ @Test
+ void testGetAddressNotFound() {
+ when(processHandle.info()).thenReturn(processHandleInfo);
+
+ final Optional managementServerAddress = provider.getAddress();
+
+ assertTrue(managementServerAddress.isEmpty());
+ }
+
+ @Test
+ void testGetAddressArgumentNotFound() {
+ when(processHandle.info()).thenReturn(processHandleInfo);
+ when(processHandleInfo.arguments()).thenReturn(Optional.empty());
+
+ final Optional managementServerAddress = provider.getAddress();
+
+ assertTrue(managementServerAddress.isEmpty());
+ }
+
+ @Test
+ void testGetAddress() {
+ when(processHandle.info()).thenReturn(processHandleInfo);
+
+ final String systemPropertyArgument = SYSTEM_PROPERTY_ARGUMENT.formatted(SystemProperty.MANAGEMENT_SERVER_ADDRESS.getProperty(), ADDRESS);
+ final String[] arguments = new String[]{systemPropertyArgument};
+
+ when(processHandleInfo.arguments()).thenReturn(Optional.of(arguments));
+
+ final Optional managementServerAddress = provider.getAddress();
+
+ assertTrue(managementServerAddress.isPresent());
+
+ final String address = managementServerAddress.get();
+ assertEquals(ADDRESS, address);
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/StandardManagementServerAddressProviderTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/StandardManagementServerAddressProviderTest.java
new file mode 100644
index 000000000000..3581423b2c7e
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/StandardManagementServerAddressProviderTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(MockitoExtension.class)
+class StandardManagementServerAddressProviderTest {
+ @Mock
+ private ConfigurationProvider configurationProvider;
+
+ private StandardManagementServerAddressProvider provider;
+
+ @BeforeEach
+ void setProvider() {
+ provider = new StandardManagementServerAddressProvider(configurationProvider);
+ }
+
+ @Test
+ void testGetAddressFound() {
+ final Optional managementServerAddress = provider.getAddress();
+
+ assertTrue(managementServerAddress.isPresent());
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/StandardProcessBuilderProviderTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/StandardProcessBuilderProviderTest.java
new file mode 100644
index 000000000000..b1cc3c705df4
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/StandardProcessBuilderProviderTest.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import org.apache.nifi.bootstrap.configuration.ApplicationClassName;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.apache.nifi.bootstrap.configuration.SystemProperty;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class StandardProcessBuilderProviderTest {
+ private static final String SERVER_ADDRESS = "127.0.0.1:52020";
+
+ private static final String SERVER_ADDRESS_PROPERTY = "-D%s=%s".formatted(SystemProperty.MANAGEMENT_SERVER_ADDRESS.getProperty(), SERVER_ADDRESS);
+
+ @Mock
+ private ConfigurationProvider configurationProvider;
+
+ @Mock
+ private ManagementServerAddressProvider managementServerAddressProvider;
+
+ private StandardProcessBuilderProvider provider;
+
+ @BeforeEach
+ void setProvider() {
+ provider = new StandardProcessBuilderProvider(configurationProvider, managementServerAddressProvider);
+ }
+
+ @Test
+ void testGetApplicationProcessBuilder(@TempDir final Path workingDirectory) {
+ when(configurationProvider.getLibraryDirectory()).thenReturn(workingDirectory);
+ when(configurationProvider.getConfigurationDirectory()).thenReturn(workingDirectory);
+ when(managementServerAddressProvider.getAddress()).thenReturn(Optional.of(SERVER_ADDRESS));
+
+ final ProcessBuilder processBuilder = provider.getApplicationProcessBuilder();
+
+ assertNotNull(processBuilder);
+
+ final List command = processBuilder.command();
+
+ final String currentCommand = ProcessHandle.current().info().command().orElse(null);
+ final String firstCommand = command.getFirst();
+ assertEquals(currentCommand, firstCommand);
+
+ final String lastCommand = command.getLast();
+ assertEquals(ApplicationClassName.APPLICATION.getName(), lastCommand);
+
+ assertTrue(command.contains(SERVER_ADDRESS_PROPERTY));
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/StandardProcessHandleProviderTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/StandardProcessHandleProviderTest.java
new file mode 100644
index 000000000000..8e73b66c2e65
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/command/process/StandardProcessHandleProviderTest.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.command.process;
+
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(MockitoExtension.class)
+class StandardProcessHandleProviderTest {
+ @Mock
+ private ConfigurationProvider configurationProvider;
+
+ private StandardProcessHandleProvider provider;
+
+ @BeforeEach
+ void setProvider() {
+ provider = new StandardProcessHandleProvider(configurationProvider);
+ }
+
+ @Test
+ void testFindApplicationProcessHandleEmpty() {
+ final Optional applicationProcessHandle = provider.findApplicationProcessHandle();
+
+ assertTrue(applicationProcessHandle.isEmpty());
+ }
+
+ @Test
+ void testFindBootstrapProcessHandleEmpty() {
+ final Optional bootstrapProcessHandle = provider.findBootstrapProcessHandle();
+
+ assertTrue(bootstrapProcessHandle.isEmpty());
+ }
+}
diff --git a/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/configuration/StandardConfigurationProviderTest.java b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/configuration/StandardConfigurationProviderTest.java
new file mode 100644
index 000000000000..74af0687b016
--- /dev/null
+++ b/nifi-bootstrap/src/test/java/org/apache/nifi/bootstrap/configuration/StandardConfigurationProviderTest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.bootstrap.configuration;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StandardConfigurationProviderTest {
+
+ private static final String CONFIGURATION_DIRECTORY = "conf";
+
+ private static final String BOOTSTRAP_CONFIGURATION = "bootstrap.conf";
+
+ private static final String CURRENT_DIRECTORY = "";
+
+ private static final String MANAGEMENT_SERVER_ADDRESS = "http://127.0.0.1:52020";
+
+ private final Map environmentVariables = new LinkedHashMap<>();
+
+ private final Properties systemProperties = new Properties();
+
+ @BeforeEach
+ void setProvider() {
+ environmentVariables.clear();
+ systemProperties.clear();
+ }
+
+ @Test
+ void testApplicationHomeNotConfigured() {
+ assertThrows(IllegalStateException.class, () -> new StandardConfigurationProvider(environmentVariables, systemProperties));
+ }
+
+ @Test
+ void testGetBootstrapConfiguration(@TempDir final Path applicationHomeDirectory) throws IOException {
+ environmentVariables.put(EnvironmentVariable.NIFI_HOME.name(), applicationHomeDirectory.toString());
+ final Path configurationDirectory = createConfigurationDirectory(applicationHomeDirectory);
+ final Path bootstrapConfiguration = setRequiredConfiguration(applicationHomeDirectory);
+
+ final StandardConfigurationProvider provider = new StandardConfigurationProvider(environmentVariables, systemProperties);
+
+ final Path configurationDirectoryProvided = provider.getConfigurationDirectory();
+ assertEquals(configurationDirectory, configurationDirectoryProvided);
+
+ final Path bootstrapConfigurationProvided = provider.getBootstrapConfiguration();
+ assertEquals(bootstrapConfiguration, bootstrapConfigurationProvided);
+ }
+
+ @Test
+ void testGetWorkingDirectory(@TempDir final Path applicationHomeDirectory) throws IOException {
+ setRequiredConfiguration(applicationHomeDirectory);
+
+ final StandardConfigurationProvider provider = new StandardConfigurationProvider(environmentVariables, systemProperties);
+
+ final Path workingDirectory = provider.getWorkingDirectory();
+ final Path workingDirectoryExpected = Paths.get(CURRENT_DIRECTORY).toAbsolutePath();
+
+ assertEquals(workingDirectoryExpected, workingDirectory);
+ }
+
+ @Test
+ void testGetManagementServerAddressNotConfigured(@TempDir final Path applicationHomeDirectory) throws IOException {
+ setRequiredConfiguration(applicationHomeDirectory);
+
+ final StandardConfigurationProvider provider = new StandardConfigurationProvider(environmentVariables, systemProperties);
+
+ final Optional managementServerAddress = provider.getManagementServerAddress();
+
+ assertTrue(managementServerAddress.isEmpty());
+ }
+
+ @Test
+ void testGetManagementServerAddress(@TempDir final Path applicationHomeDirectory) throws IOException {
+ final Path bootstrapConfiguration = setRequiredConfiguration(applicationHomeDirectory);
+
+ final Properties bootstrapProperties = new Properties();
+ bootstrapProperties.put(BootstrapProperty.MANAGEMENT_SERVER_ADDRESS.getProperty(), MANAGEMENT_SERVER_ADDRESS);
+ try (OutputStream outputStream = Files.newOutputStream(bootstrapConfiguration)) {
+ bootstrapProperties.store(outputStream, Properties.class.getSimpleName());
+ }
+
+ final StandardConfigurationProvider provider = new StandardConfigurationProvider(environmentVariables, systemProperties);
+
+ final Optional managementServerAddress = provider.getManagementServerAddress();
+
+ assertTrue(managementServerAddress.isPresent());
+ final URI address = managementServerAddress.get();
+ assertEquals(MANAGEMENT_SERVER_ADDRESS, address.toString());
+ }
+
+ private Path setRequiredConfiguration(final Path applicationHomeDirectory) throws IOException {
+ environmentVariables.put(EnvironmentVariable.NIFI_HOME.name(), applicationHomeDirectory.toString());
+ final Path configurationDirectory = createConfigurationDirectory(applicationHomeDirectory);
+ return createBootstrapConfiguration(configurationDirectory);
+ }
+
+ private Path createConfigurationDirectory(final Path applicationHomeDirectory) {
+ final Path configurationDirectory = applicationHomeDirectory.resolve(CONFIGURATION_DIRECTORY);
+ if (configurationDirectory.toFile().mkdir()) {
+ assertTrue(Files.isReadable(configurationDirectory));
+ }
+ return configurationDirectory;
+ }
+
+ private Path createBootstrapConfiguration(final Path configurationDirectory) throws IOException {
+ final Path bootstrapConfiguration = configurationDirectory.resolve(BOOTSTRAP_CONFIGURATION);
+ assertTrue(bootstrapConfiguration.toFile().createNewFile());
+ return bootstrapConfiguration;
+ }
+}
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 6ab2fea53f3c..922ab26c686d 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -2842,7 +2842,6 @@ properties for minimum and maximum Java Heap size, the garbage collector to use,
|`nifi.diagnostics.on.shutdown.directory`|This property specifies the location of the NiFi diagnostics directory. The default value is `./diagnostics`.
|`nifi.diagnostics.on.shutdown.max.filecount`|This property specifies the maximum permitted number of diagnostic files. If the limit is exceeded, the oldest files are deleted. The default value is `10`.
|`nifi.diagnostics.on.shutdown.max.directory.size`|This property specifies the maximum permitted size of the diagnostics directory. If the limit is exceeded, the oldest files are deleted. The default value is `10 MB`.
-|`nifi.bootstrap.listen.port`|This property defines the port used to listen for communications from NiFi. If this property is missing, empty, or `0`, a random ephemeral port is used.
|====
[[proxy_configuration]]
diff --git a/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/bin/nifi.cmd b/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/bin/nifi.cmd
index 157cad3295db..6a5748760677 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/bin/nifi.cmd
+++ b/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/bin/nifi.cmd
@@ -29,17 +29,16 @@ set BOOTSTRAP_LIB_DIR=%NIFI_HOME%\lib\bootstrap
set CONF_DIR=%NIFI_HOME%\conf
set LOG_DIR_PROPERTY=-Dorg.apache.nifi.bootstrap.config.log.dir=%NIFI_LOG_DIR%
-set PID_DIR_PROPERTY=-Dorg.apache.nifi.bootstrap.config.pid.dir=%NIFI_PID_DIR%
set CONFIG_FILE_PROPERTY=-Dorg.apache.nifi.bootstrap.config.file=%CONF_DIR%\bootstrap.conf
set PROPERTIES_FILE_PROPERTY=-Dnifi.properties.file.path=%CONF_DIR%\nifi.properties
set BOOTSTRAP_HEAP_SIZE=48m
-set JAVA_ARGS=%LOG_DIR_PROPERTY% %PID_DIR_PROPERTY% %CONFIG_FILE_PROPERTY% %PROPERTIES_FILE_PROPERTY%
+set JAVA_ARGS=%LOG_DIR_PROPERTY% %CONFIG_FILE_PROPERTY% %PROPERTIES_FILE_PROPERTY%
set JAVA_PARAMS=-cp %BOOTSTRAP_LIB_DIR%\*;%CONF_DIR% %JAVA_ARGS%
set JAVA_MEMORY=-Xms%BOOTSTRAP_HEAP_SIZE% -Xmx%BOOTSTRAP_HEAP_SIZE%
-echo JAVA_HOME: %JAVA_HOME%
-echo NIFI_HOME: %NIFI_HOME%
+echo JAVA_HOME=%JAVA_HOME%
+echo NIFI_HOME=%NIFI_HOME%
echo.
pushd %NIFI_HOME%
@@ -54,7 +53,7 @@ if %RUN_COMMAND% == "set-single-user-credentials" (
) else if %RUN_COMMAND% == "set-sensitive-properties-algorithm" (
call "%JAVA_EXE%" %JAVA_PARAMS% org.apache.nifi.flow.encryptor.command.SetSensitivePropertiesAlgorithm %~2
) else (
- call "%JAVA_EXE%" %JAVA_MEMORY% %JAVA_PARAMS% org.apache.nifi.bootstrap.RunNiFi %RUN_COMMAND%
+ call "%JAVA_EXE%" %JAVA_MEMORY% %JAVA_PARAMS% org.apache.nifi.bootstrap.BootstrapProcess %RUN_COMMAND%
)
popd
diff --git a/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/bin/nifi.sh b/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/bin/nifi.sh
index d8d406fab44e..30474bdd076e 100755
--- a/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/bin/nifi.sh
+++ b/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/bin/nifi.sh
@@ -143,15 +143,6 @@ locateJava() {
fi
fi
fi
- # if command is env, attempt to add more to the classpath
- if [ "$1" = "env" ]; then
- [ "x${TOOLS_JAR}" = "x" ] && [ -n "${JAVA_HOME}" ] && TOOLS_JAR=$(find -H "${JAVA_HOME}" -name "tools.jar")
- [ "x${TOOLS_JAR}" = "x" ] && [ -n "${JAVA_HOME}" ] && TOOLS_JAR=$(find -H "${JAVA_HOME}" -name "classes.jar")
- if [ "x${TOOLS_JAR}" = "x" ]; then
- warn "Could not locate tools.jar or classes.jar. Please set manually to avail all command features."
- fi
- fi
-
}
init() {
@@ -162,7 +153,7 @@ init() {
unlimitFD
# Locate the Java VM to execute
- locateJava "$1"
+ locateJava
}
is_nonzero_integer() {
@@ -197,7 +188,6 @@ run() {
NIFI_HOME=$(cygpath --path --windows "${NIFI_HOME}")
NIFI_LOG_DIR=$(cygpath --path --windows "${NIFI_LOG_DIR}")
- NIFI_PID_DIR=$(cygpath --path --windows "${NIFI_PID_DIR}")
BOOTSTRAP_CONF=$(cygpath --path --windows "${BOOTSTRAP_CONF}")
BOOTSTRAP_CONF_DIR=$(cygpath --path --windows "${BOOTSTRAP_CONF_DIR}")
BOOTSTRAP_LIBS=$(cygpath --path --windows "${BOOTSTRAP_LIBS}")
@@ -220,26 +210,21 @@ run() {
fi
echo
- echo "Java home: ${JAVA_HOME}"
- echo "NiFi home: ${NIFI_HOME}"
- echo
- echo "Bootstrap Config File: ${BOOTSTRAP_CONF}"
+ echo "JAVA_HOME=${JAVA_HOME}"
+ echo "NIFI_HOME=${NIFI_HOME}"
echo
- # run 'start' in the background because the process will continue to run, monitoring NiFi.
- # all other commands will terminate quickly so want to just wait for them
-
#setup directory parameters
BOOTSTRAP_LOG_PARAMS="-Dorg.apache.nifi.bootstrap.config.log.dir='${NIFI_LOG_DIR}'"
- BOOTSTRAP_PID_PARAMS="-Dorg.apache.nifi.bootstrap.config.pid.dir='${NIFI_PID_DIR}'"
BOOTSTRAP_CONF_PARAMS="-Dorg.apache.nifi.bootstrap.config.file='${BOOTSTRAP_CONF}'"
# uncomment to allow debugging of the bootstrap process
#BOOTSTRAP_DEBUG_PARAMS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000"
- BOOTSTRAP_DIR_PARAMS="${BOOTSTRAP_LOG_PARAMS} ${BOOTSTRAP_PID_PARAMS} ${BOOTSTRAP_CONF_PARAMS}"
+ BOOTSTRAP_DIR_PARAMS="${BOOTSTRAP_LOG_PARAMS} ${BOOTSTRAP_CONF_PARAMS}"
- run_bootstrap_cmd="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' -Xms48m -Xmx48m ${BOOTSTRAP_DIR_PARAMS} ${BOOTSTRAP_DEBUG_PARAMS} ${BOOTSTRAP_JAVA_OPTS} org.apache.nifi.bootstrap.RunNiFi"
+ MAXIMUM_HEAP_SIZE="-Xmx48m"
+ run_bootstrap_cmd="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' ${MAXIMUM_HEAP_SIZE} ${BOOTSTRAP_DIR_PARAMS} ${BOOTSTRAP_DEBUG_PARAMS} ${BOOTSTRAP_JAVA_OPTS} org.apache.nifi.bootstrap.BootstrapProcess"
run_nifi_cmd="${run_bootstrap_cmd} $@"
if [ -n "${run_as_user}" ]; then
@@ -252,11 +237,6 @@ run() {
run_nifi_cmd="${SUDO} -u ${run_as_user} sh -c \"SCRIPT_DIR='${SCRIPT_DIR}' && . '${SCRIPT_DIR}/nifi-env.sh' && ${run_nifi_cmd}\""
fi
- if [ "$1" = "run" ]; then
- # Use exec to handover PID to RunNiFi java process, instead of foking it as a child process
- run_nifi_cmd="exec ${run_nifi_cmd}"
- fi
-
if [ "$1" = "set-sensitive-properties-algorithm" ]; then
run_command="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' '-Dnifi.properties.file.path=${NIFI_HOME}/conf/nifi.properties' 'org.apache.nifi.flow.encryptor.command.SetSensitivePropertiesAlgorithm'"
eval "cd ${NIFI_HOME}"
@@ -287,8 +267,20 @@ run() {
return;
fi
- if [ "$1" = "start" ]; then
- ( eval "cd ${NIFI_HOME} && ${run_nifi_cmd}" & )> /dev/null 1>&-
+ eval "cd ${NIFI_HOME}"
+
+ if [ "$1" = "run" ]; then
+ RUN_COMMAND=$(eval "${run_bootstrap_cmd} get-run-command")
+ RUN_COMMAND_STATUS=$?
+ if [ $RUN_COMMAND_STATUS = 0 ]; then
+ exec $RUN_COMMAND
+ else
+ echo "Failed to get run command"
+ echo "${RUN_COMMAND}"
+ exit 1
+ fi
+ elif [ "$1" = "start" ]; then
+ eval "${run_nifi_cmd}" > /dev/null 1>&- &
if [ "$2" = "--wait-for-init" ]; then
@@ -304,63 +296,57 @@ run() {
time_since_feedback=0
not_running_counter=0
- is_nifi_loaded="false" # 3 possible values: "true", "false", "not_running". "not_running" means NiFi has not been started.
- while [ "$is_nifi_loaded" != "true" ]; do
+ PROCESS_STATUS=1
+ while [ $PROCESS_STATUS != 0 ]; do
time_at_previous_loop=$current_time
current_time=$(date +%s)
if [ "$current_time" -ge "$endtime" ]; then
- echo "Exited the script due to --wait-for-init timeout"
+ echo "Initialization failed after $wait_timeout seconds"
break;
fi
time_since_feedback=$(($time_since_feedback+($current_time-$time_at_previous_loop)))
if [ "$time_since_feedback" -ge "$WAIT_FOR_INIT_FEEDBACK_INTERVAL" ]; then
time_since_feedback=0
- echo "NiFi has not fully initialized yet..."
fi
- is_nifi_loaded=$( eval "cd ${NIFI_HOME} && ${run_bootstrap_cmd} is_loaded" )
+ eval "cd ${NIFI_HOME} && ${run_bootstrap_cmd} status"
+ PROCESS_STATUS=$?
- if [ "$is_nifi_loaded" = "not_running" ]; then
+ if [ $PROCESS_STATUS = 3 ]; then
not_running_counter=$(($not_running_counter+1))
if [ "$not_running_counter" -ge 3 ]; then
- echo "NiFi is not running. Stopped waiting for it to initialize."
+ echo "Initialization failed"
break;
fi
fi
sleep $WAIT_FOR_INIT_SLEEP_TIME
done
- if [ "$is_nifi_loaded" = "true" ]; then
- echo "NiFi initialized."
- echo "Exiting startup script..."
+ if [ $PROCESS_STATUS = 0 ]; then
+ echo "Initialization completed"
fi
fi
+
+ # Wait for logging initialization before returning to shell after starting
+ sleep 1
else
- eval "cd ${NIFI_HOME} && ${run_nifi_cmd}"
+ eval "${run_nifi_cmd}"
fi
EXIT_STATUS=$?
- # Wait just a bit (3 secs) to wait for the logging to finish and then echo a new-line.
- # We do this to avoid having logs spewed on the console after running the command and then not giving
- # control back to the user
- sleep 1
echo
}
main() {
- init "$1"
+ init
run "$@"
}
case "$1" in
- install)
- install "$@"
- ;;
-
- start|stop|decommission|run|status|is_loaded|dump|diagnostics|status-history|env|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials|cluster-status)
+ start|stop|decommission|run|status|cluster-status|diagnostics|status-history|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials)
main "$@"
;;
@@ -370,6 +356,6 @@ case "$1" in
run "start"
;;
*)
- echo "Usage nifi {start|stop|decommission|run|restart|status|dump|diagnostics|status-history|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials|cluster-status}"
+ echo "Usage nifi.sh {start|stop|decommission|run|restart|status|cluster-status|diagnostics|status-history|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials}"
;;
esac
diff --git a/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap.conf b/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap.conf
index 53094181b79b..09394b7afd9e 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap.conf
+++ b/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/bootstrap.conf
@@ -15,9 +15,6 @@
# limitations under the License.
#
-# Java command to use when running NiFi
-java=java
-
# Username to use when running NiFi. This value will be ignored on Windows.
run.as=${nifi.run.as}
@@ -61,5 +58,3 @@ java.arg.securityAuthUseSubjectCredsOnly=-Djavax.security.auth.useSubjectCredsOn
# org.apache.jasper.servlet.JasperLoader,org.jvnet.hk2.internal.DelegatingClassLoader,org.apache.nifi.nar.NarClassLoader
# End of Java Agent config for native library loading.
-# Port used to listen for communications from NiFi. If this property is missing, empty, or 0, a random ephemeral port is used.
-nifi.bootstrap.listen.port=0
diff --git a/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/logback.xml b/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/logback.xml
index cbeff47ef6bb..baef9d3881c6 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/logback.xml
+++ b/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/logback.xml
@@ -15,8 +15,6 @@
-->
-
-
true
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/BootstrapListener.java b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/BootstrapListener.java
deleted file mode 100644
index d6537603d7fa..000000000000
--- a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/BootstrapListener.java
+++ /dev/null
@@ -1,406 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi;
-
-import org.apache.nifi.cluster.ClusterDetailsFactory;
-import org.apache.nifi.cluster.ConnectionState;
-import org.apache.nifi.controller.DecommissionTask;
-import org.apache.nifi.controller.status.history.StatusHistoryDump;
-import org.apache.nifi.diagnostics.DiagnosticsDump;
-import org.apache.nifi.util.LimitingInputStream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.InetSocketAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.SocketTimeoutException;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.UUID;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class BootstrapListener {
-
- private static final Logger logger = LoggerFactory.getLogger(BootstrapListener.class);
-
- private final NiFiEntryPoint nifi;
- private final int bootstrapPort;
- private final String secretKey;
-
- private volatile Listener listener;
- private volatile ServerSocket serverSocket;
- private volatile boolean nifiLoaded = false;
-
- public BootstrapListener(final NiFiEntryPoint nifi, final int bootstrapPort) {
- this.nifi = nifi;
- this.bootstrapPort = bootstrapPort;
- secretKey = UUID.randomUUID().toString();
- }
-
- public void start(final int listenPort) throws IOException {
- logger.debug("Starting Bootstrap Listener to communicate with Bootstrap Port {}", bootstrapPort);
-
- serverSocket = new ServerSocket();
- serverSocket.bind(new InetSocketAddress("localhost", listenPort));
- serverSocket.setSoTimeout(2000);
-
- final int localPort = serverSocket.getLocalPort();
- logger.info("Started Bootstrap Listener, Listening for incoming requests on port {}", localPort);
-
- listener = new Listener(serverSocket);
- final Thread listenThread = new Thread(listener);
- listenThread.setDaemon(true);
- listenThread.setName("Listen to Bootstrap");
- listenThread.start();
-
- logger.debug("Notifying Bootstrap that local port is {}", localPort);
- sendCommand("PORT", new String[]{String.valueOf(localPort), secretKey});
- }
-
- public void reload() throws IOException {
- if (listener != null) {
- listener.stop();
- }
- sendCommand("RELOAD", new String[]{});
- }
-
- public void stop() {
- if (listener != null) {
- listener.stop();
- }
- }
-
- public void setNiFiLoaded(boolean nifiLoaded) {
- this.nifiLoaded = nifiLoaded;
- }
-
- public void sendStartedStatus(boolean status) throws IOException {
- logger.debug("Notifying Bootstrap that the status of starting NiFi is {}", status);
- sendCommand("STARTED", new String[]{String.valueOf(status)});
- }
-
- private void sendCommand(final String command, final String[] args) throws IOException {
- try (final Socket socket = new Socket()) {
- socket.setSoTimeout(60000);
- socket.connect(new InetSocketAddress("localhost", bootstrapPort));
- socket.setSoTimeout(60000);
-
- final StringBuilder commandBuilder = new StringBuilder(command);
- for (final String arg : args) {
- commandBuilder.append(" ").append(arg);
- }
- commandBuilder.append("\n");
-
- final String commandWithArgs = commandBuilder.toString();
- logger.debug("Sending command to Bootstrap: {}", commandWithArgs);
-
- final OutputStream out = socket.getOutputStream();
- out.write((commandWithArgs).getBytes(StandardCharsets.UTF_8));
- out.flush();
-
- logger.debug("Awaiting response from Bootstrap...");
- final BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- final String response = reader.readLine();
- if ("OK".equals(response)) {
- logger.info("Successfully initiated communication with Bootstrap");
- } else {
- logger.error("Failed to communicate with Bootstrap. Bootstrap may be unable to issue or receive commands from NiFi");
- }
- }
- }
-
- private class Listener implements Runnable {
-
- private final ServerSocket serverSocket;
- private final ExecutorService executor;
- private volatile boolean stopped = false;
-
- public Listener(final ServerSocket serverSocket) {
- this.serverSocket = serverSocket;
- this.executor = Executors.newFixedThreadPool(2);
- }
-
- public void stop() {
- stopped = true;
-
- executor.shutdownNow();
-
- try {
- serverSocket.close();
- } catch (final IOException ioe) {
- // nothing to really do here. we could log this, but it would just become
- // confusing in the logs, as we're shutting down and there's no real benefit
- }
- }
-
- @Override
- public void run() {
- while (!stopped) {
- try {
- final Socket socket;
- try {
- logger.debug("Listening for Bootstrap Requests");
- socket = serverSocket.accept();
- } catch (final SocketTimeoutException ste) {
- if (stopped) {
- return;
- }
-
- continue;
- } catch (final IOException ioe) {
- if (stopped) {
- return;
- }
-
- throw ioe;
- }
-
- logger.debug("Received connection from Bootstrap");
- socket.setSoTimeout(5000);
-
- executor.submit(new Runnable() {
- @Override
- public void run() {
- try {
- final BootstrapRequest request = readRequest(socket.getInputStream());
- final BootstrapRequest.RequestType requestType = request.getRequestType();
-
- switch (requestType) {
- case PING:
- logger.debug("Received PING request from Bootstrap; responding");
- sendAnswer(socket.getOutputStream(), "PING");
- logger.debug("Responded to PING request from Bootstrap");
- break;
- case RELOAD:
- logger.info("Received RELOAD request from Bootstrap");
- sendAnswer(socket.getOutputStream(), "RELOAD");
- nifi.shutdownHook(true);
- return;
- case SHUTDOWN:
- logger.info("Received SHUTDOWN request from Bootstrap");
- sendAnswer(socket.getOutputStream(), "SHUTDOWN");
- socket.close();
- nifi.shutdownHook(false);
- return;
- case DUMP:
- logger.info("Received DUMP request from Bootstrap");
- writeDump(socket.getOutputStream());
- break;
- case CLUSTER_STATUS:
- logger.info("Received CLUSTER_STATUS request from Bootstrap");
- final String clusterStatus = getClusterStatus();
- logger.debug("Responding to CLUSTER_STATUS request from Bootstrap with {}", clusterStatus);
- sendAnswer(socket.getOutputStream(), clusterStatus);
- break;
- case DECOMMISSION:
- logger.info("Received DECOMMISSION request from Bootstrap");
-
- boolean shutdown = true;
- final String[] decommissionArgs = request.getArgs();
- if (decommissionArgs != null) {
- for (final String arg : decommissionArgs) {
- if ("--shutdown=false".equalsIgnoreCase(arg)) {
- shutdown = false;
- break;
- }
- }
- }
-
- logger.info("Command indicates that after decommission, shutdown={}", shutdown);
-
- try {
- decommission();
- sendAnswer(socket.getOutputStream(), "DECOMMISSION");
-
- if (shutdown) {
- nifi.shutdownHook(false);
- }
- } catch (final Exception e) {
- final OutputStream out = socket.getOutputStream();
-
- out.write(("Failed to decommission node: " + e + "; see app-log for additional details").getBytes(StandardCharsets.UTF_8));
- out.flush();
- } finally {
- if (shutdown) {
- socket.close();
- }
- }
-
- break;
- case DIAGNOSTICS:
- logger.info("Received DIAGNOSTICS request from Bootstrap");
- final String[] args = request.getArgs();
- boolean verbose = false;
- if (args == null) {
- verbose = false;
- } else {
- for (final String arg : args) {
- if ("--verbose=true".equalsIgnoreCase(arg)) {
- verbose = true;
- break;
- }
- }
- }
-
- writeDiagnostics(socket.getOutputStream(), verbose);
- break;
- case STATUS_HISTORY:
- logger.info("Received STATUS_HISTORY request from Bootstrap");
- final String[] statusHistoryArgs = request.getArgs();
- final int days = Integer.parseInt(statusHistoryArgs[0]);
- writeNodeStatusHistory(socket.getOutputStream(), days);
- break;
- case IS_LOADED:
- logger.debug("Received IS_LOADED request from Bootstrap");
- String answer = String.valueOf(nifiLoaded);
- sendAnswer(socket.getOutputStream(), answer);
- logger.debug("Responded to IS_LOADED request from Bootstrap with value: {}", answer);
- break;
- }
- } catch (final Throwable t) {
- logger.error("Failed to process request from Bootstrap", t);
- } finally {
- try {
- socket.close();
- } catch (final IOException ioe) {
- logger.warn("Failed to close socket to Bootstrap", ioe);
- }
- }
- }
- });
- } catch (final Throwable t) {
- logger.error("Failed to process request from Bootstrap", t);
- }
- }
- }
- }
-
- private void writeDump(final OutputStream out) throws IOException {
- final DiagnosticsDump diagnosticsDump = nifi.getServer().getThreadDumpFactory().create(true);
- diagnosticsDump.writeTo(out);
- }
-
- private String getClusterStatus() {
- final ClusterDetailsFactory clusterDetailsFactory = nifi.getServer().getClusterDetailsFactory();
- if (clusterDetailsFactory == null) {
- return ConnectionState.UNKNOWN.name();
- }
-
- final ConnectionState connectionState = clusterDetailsFactory.getConnectionState();
- return connectionState == null ? ConnectionState.UNKNOWN.name() : connectionState.name();
- }
-
- private void decommission() throws InterruptedException {
- final DecommissionTask decommissionTask = nifi.getServer().getDecommissionTask();
- if (decommissionTask == null) {
- throw new IllegalArgumentException("This NiFi instance does not support decommissioning");
- }
-
- decommissionTask.decommission();
- }
-
- private void writeDiagnostics(final OutputStream out, final boolean verbose) throws IOException {
- final DiagnosticsDump diagnosticsDump = nifi.getServer().getDiagnosticsFactory().create(verbose);
- diagnosticsDump.writeTo(out);
- }
-
- private void writeNodeStatusHistory(final OutputStream out, final int days) throws IOException {
- final StatusHistoryDump statusHistoryDump = nifi.getServer().getStatusHistoryDumpFactory().create(days);
- statusHistoryDump.writeTo(out);
- }
-
- private void sendAnswer(final OutputStream out, final String answer) throws IOException {
- out.write((answer + "\n").getBytes(StandardCharsets.UTF_8));
- out.flush();
- }
-
- @SuppressWarnings("resource") // we don't want to close the stream, as the caller will do that
- private BootstrapRequest readRequest(final InputStream in) throws IOException {
- // We want to ensure that we don't try to read data from an InputStream directly
- // by a BufferedReader because any user on the system could open a socket and send
- // a multi-gigabyte file without any new lines in order to crash the NiFi instance
- // (or at least cause OutOfMemoryErrors, which can wreak havoc on the running instance).
- // So we will limit the Input Stream to only 4 KB, which should be plenty for any request.
- final LimitingInputStream limitingIn = new LimitingInputStream(in, 4096);
- final BufferedReader reader = new BufferedReader(new InputStreamReader(limitingIn));
-
- final String line = reader.readLine();
- final String[] splits = line.split(" ");
- if (splits.length < 1) {
- throw new IOException("Received invalid request from Bootstrap: " + line);
- }
-
- final String requestType = splits[0];
- final String[] args;
- if (splits.length == 1) {
- throw new IOException("Received invalid request from Bootstrap; request did not have a secret key; request type = " + requestType);
- } else if (splits.length == 2) {
- args = new String[0];
- } else {
- args = Arrays.copyOfRange(splits, 2, splits.length);
- }
-
- final String requestKey = splits[1];
- if (!secretKey.equals(requestKey)) {
- throw new IOException("Received invalid Secret Key for request type " + requestType);
- }
-
- try {
- return new BootstrapRequest(requestType, args);
- } catch (final Exception e) {
- throw new IOException("Received invalid request from Bootstrap; request type = " + requestType);
- }
- }
-
- private static class BootstrapRequest {
- public enum RequestType {
- RELOAD,
- SHUTDOWN,
- DUMP,
- DIAGNOSTICS,
- DECOMMISSION,
- PING,
- IS_LOADED,
- STATUS_HISTORY,
- CLUSTER_STATUS
- }
-
- private final RequestType requestType;
- private final String[] args;
-
- public BootstrapRequest(final String request, final String[] args) {
- this.requestType = RequestType.valueOf(request);
- this.args = args;
- }
-
- public RequestType getRequestType() {
- return requestType;
- }
-
- @SuppressWarnings("unused")
- public String[] getArgs() {
- return args;
- }
- }
-}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFi.java b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFi.java
index deb7529967b4..c21ec70bd0cd 100644
--- a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFi.java
+++ b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/NiFi.java
@@ -25,6 +25,8 @@
import org.apache.nifi.nar.NarUnpacker;
import org.apache.nifi.nar.SystemBundle;
import org.apache.nifi.processor.DataUnit;
+import org.apache.nifi.runtime.ManagementServer;
+import org.apache.nifi.runtime.StandardManagementServer;
import org.apache.nifi.util.DiagnosticUtils;
import org.apache.nifi.util.FileUtils;
import org.apache.nifi.util.NiFiProperties;
@@ -38,6 +40,7 @@
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
@@ -50,19 +53,32 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Stream;
-public class NiFi implements NiFiEntryPoint {
+public class NiFi {
- public static final String BOOTSTRAP_PORT_PROPERTY = "nifi.bootstrap.listen.port";
- public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
+
+ private static final String MANAGEMENT_SERVER_ADDRESS = "org.apache.nifi.management.server.address";
+
+ private static final Pattern MANAGEMENT_SERVER_ADDRESS_PATTERN = Pattern.compile("^(.+?):([1-9][0-9]{3,4})$");
+
+ private static final String MANAGEMENT_SERVER_DEFAULT_ADDRESS = "127.0.0.1:52020";
+
+ private static final int ADDRESS_GROUP = 1;
+
+ private static final int PORT_GROUP = 2;
private static final Logger LOGGER = LoggerFactory.getLogger(NiFi.class);
private final NiFiServer nifiServer;
- private final BootstrapListener bootstrapListener;
+
private final NiFiProperties properties;
+ private final ManagementServer managementServer;
+
private volatile boolean shutdown = false;
public NiFi(final NiFiProperties properties)
@@ -89,25 +105,6 @@ public NiFi(final NiFiProperties properties, ClassLoader rootClassLoader)
// register the shutdown hook
addShutdownHook();
- final String bootstrapPort = System.getProperty(BOOTSTRAP_PORT_PROPERTY);
- if (bootstrapPort != null) {
- try {
- final int port = Integer.parseInt(bootstrapPort);
-
- if (port < 1 || port > 65535) {
- throw new RuntimeException("Failed to start NiFi because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535");
- }
-
- bootstrapListener = new BootstrapListener(this, port);
- bootstrapListener.start(properties.getDefaultListenerBootstrapPort());
- } catch (final NumberFormatException nfe) {
- throw new RuntimeException("Failed to start NiFi because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535");
- }
- } else {
- LOGGER.info("NiFi started without Bootstrap Port information provided; will not listen for requests from Bootstrap");
- bootstrapListener = null;
- }
-
// delete the web working dir - if the application does not start successfully
// the web app directories might be in an invalid state. when this happens
// jetty will not attempt to re-extract the war into the directory. by removing
@@ -151,15 +148,12 @@ public NiFi(final NiFiProperties properties, ClassLoader rootClassLoader)
narBundles,
extensionMapping);
+ managementServer = getManagementServer();
if (shutdown) {
LOGGER.info("NiFi has been shutdown via NiFi Bootstrap. Will not start Controller");
} else {
nifiServer.start();
-
- if (bootstrapListener != null) {
- bootstrapListener.setNiFiLoaded(true);
- bootstrapListener.sendStartedStatus(true);
- }
+ managementServer.start();
final long duration = System.nanoTime() - startTime;
final double durationSeconds = TimeUnit.NANOSECONDS.toMillis(duration) / 1000.0;
@@ -172,14 +166,16 @@ public NiFiServer getServer() {
}
protected void setDefaultUncaughtExceptionHandler() {
- Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> LOGGER.error("An Unknown Error Occurred in Thread {}", thread, exception));
+ Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler());
}
protected void addShutdownHook() {
- Runtime.getRuntime().addShutdownHook(new Thread(() ->
- // shutdown the jetty server
- shutdownHook(false)
- ));
+ final Thread shutdownHook = Thread.ofPlatform()
+ .name(NiFi.class.getSimpleName())
+ .uncaughtExceptionHandler(new ExceptionHandler())
+ .unstarted(this::stop);
+
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
}
protected void initLogging() {
@@ -205,12 +201,15 @@ private static ClassLoader createBootstrapClassLoader() {
return new URLClassLoader(urls.toArray(new URL[0]), Thread.currentThread().getContextClassLoader());
}
- public void shutdownHook(final boolean isReload) {
+ /**
+ * Stop Application and shutdown server
+ */
+ public void stop() {
try {
runDiagnosticsOnShutdown();
shutdown();
} catch (final Throwable t) {
- LOGGER.warn("Problem occurred ensuring Jetty web server was properly terminated", t);
+ LOGGER.warn("Application Controller shutdown failed", t);
}
}
@@ -238,18 +237,39 @@ private void diagnose(final File file, final boolean verbose) throws IOException
}
}
-
protected void shutdown() {
this.shutdown = true;
- LOGGER.info("Application Server shutdown started");
- if (nifiServer != null) {
+ LOGGER.info("Application Controller shutdown started");
+
+ managementServer.stop();
+
+ if (nifiServer == null) {
+ LOGGER.info("Application Server not running");
+ } else {
nifiServer.stop();
}
- if (bootstrapListener != null) {
- bootstrapListener.stop();
+
+ LOGGER.info("Application Controller shutdown completed");
+ }
+
+ private ManagementServer getManagementServer() {
+ final String managementServerAddressProperty = System.getProperty(MANAGEMENT_SERVER_ADDRESS, MANAGEMENT_SERVER_DEFAULT_ADDRESS);
+ if (managementServerAddressProperty.isBlank()) {
+ throw new IllegalStateException("Management Server Address System Property [%s] not configured".formatted(MANAGEMENT_SERVER_ADDRESS));
+ }
+
+ final Matcher matcher = MANAGEMENT_SERVER_ADDRESS_PATTERN.matcher(managementServerAddressProperty);
+ if (matcher.matches()) {
+ final String addressGroup = matcher.group(ADDRESS_GROUP);
+ final String portGroup = matcher.group(PORT_GROUP);
+ final int port = Integer.parseInt(portGroup);
+
+ final InetSocketAddress bindAddress = new InetSocketAddress(addressGroup, port);
+ return new StandardManagementServer(bindAddress, nifiServer);
+ } else {
+ throw new IllegalStateException("Management Server Address System Property [%s] not valid [%s]".formatted(MANAGEMENT_SERVER_ADDRESS, managementServerAddressProperty));
}
- LOGGER.info("Application Server shutdown completed");
}
/**
@@ -301,4 +321,12 @@ private static NiFiProperties initializeProperties(final ClassLoader boostrapLoa
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
}
+
+ private static class ExceptionHandler implements Thread.UncaughtExceptionHandler {
+
+ @Override
+ public void uncaughtException(final Thread thread, Throwable exception) {
+ LOGGER.error("An Unknown Error Occurred in Thread {}", thread, exception);
+ }
+ }
}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthClusterHttpHandler.java b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthClusterHttpHandler.java
new file mode 100644
index 000000000000..6a11031a1b10
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthClusterHttpHandler.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.runtime;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import org.apache.nifi.NiFiServer;
+import org.apache.nifi.cluster.ClusterDetailsFactory;
+import org.apache.nifi.cluster.ConnectionState;
+import org.apache.nifi.controller.DecommissionTask;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import static java.net.HttpURLConnection.HTTP_ACCEPTED;
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+import static java.net.HttpURLConnection.HTTP_OK;
+
+/**
+ * HTTP Handler for Cluster Health status operations
+ */
+class HealthClusterHttpHandler implements HttpHandler {
+ private static final String CONTENT_TYPE_HEADER = "Content-Type";
+
+ private static final String TEXT_PLAIN = "text/plain";
+
+ private static final int NO_RESPONSE_BODY = -1;
+
+ private static final String GET_METHOD = "GET";
+
+ private static final String DELETE_METHOD = "DELETE";
+
+ private static final String STATUS = "Cluster Status: %s\n";
+
+ private final NiFiServer server;
+
+ HealthClusterHttpHandler(final NiFiServer server) {
+ this.server = Objects.requireNonNull(server);
+ }
+
+ @Override
+ public void handle(final HttpExchange exchange) throws IOException {
+ final String requestMethod = exchange.getRequestMethod();
+
+ final OutputStream responseBody = exchange.getResponseBody();
+
+ if (GET_METHOD.contentEquals(requestMethod)) {
+ exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, TEXT_PLAIN);
+ final ConnectionState connectionState = getConnectionState();
+ final String status = STATUS.formatted(connectionState);
+ final byte[] response = status.getBytes(StandardCharsets.UTF_8);
+ exchange.sendResponseHeaders(HTTP_OK, response.length);
+ responseBody.write(response);
+ } else if (DELETE_METHOD.contentEquals(requestMethod)) {
+ startDecommission();
+
+ exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, TEXT_PLAIN);
+ final String status = STATUS.formatted(ConnectionState.OFFLOADING);
+ final byte[] response = status.getBytes(StandardCharsets.UTF_8);
+ exchange.sendResponseHeaders(HTTP_ACCEPTED, response.length);
+ responseBody.write(response);
+ } else {
+ exchange.sendResponseHeaders(HTTP_BAD_METHOD, NO_RESPONSE_BODY);
+ }
+ }
+
+ private void startDecommission() {
+ final DecommissionTask decommissionTask = server.getDecommissionTask();
+ Thread.ofVirtual().name(DecommissionTask.class.getSimpleName()).start(() -> {
+ try {
+ decommissionTask.decommission();
+ } catch (final InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ }
+
+ private ConnectionState getConnectionState() {
+ final ConnectionState connectionState;
+
+ final ClusterDetailsFactory clusterDetailsFactory = server.getClusterDetailsFactory();
+ if (clusterDetailsFactory == null) {
+ connectionState = ConnectionState.UNKNOWN;
+ } else {
+ connectionState = clusterDetailsFactory.getConnectionState();
+ }
+
+ return connectionState;
+ }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthDiagnosticsHttpHandler.java b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthDiagnosticsHttpHandler.java
new file mode 100644
index 000000000000..3f4e37b81b45
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthDiagnosticsHttpHandler.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.runtime;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import org.apache.nifi.NiFiServer;
+import org.apache.nifi.diagnostics.DiagnosticsDump;
+import org.apache.nifi.diagnostics.DiagnosticsFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Objects;
+
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+import static java.net.HttpURLConnection.HTTP_OK;
+
+/**
+ * HTTP Handler for Health Diagnostics operations
+ */
+class HealthDiagnosticsHttpHandler implements HttpHandler {
+ private static final String CONTENT_TYPE_HEADER = "Content-Type";
+
+ private static final String TEXT_PLAIN = "text/plain";
+
+ private static final int STREAM_RESPONSE_BODY = 0;
+
+ private static final int NO_RESPONSE_BODY = -1;
+
+ private static final String GET_METHOD = "GET";
+
+ private static final String VERBOSE_QUERY_ENABLED = "verbose=true";
+
+ private final NiFiServer server;
+
+ HealthDiagnosticsHttpHandler(final NiFiServer server) {
+ this.server = Objects.requireNonNull(server);
+ }
+
+ @Override
+ public void handle(final HttpExchange exchange) throws IOException {
+ final String requestMethod = exchange.getRequestMethod();
+
+ if (GET_METHOD.contentEquals(requestMethod)) {
+ exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, TEXT_PLAIN);
+ exchange.sendResponseHeaders(HTTP_OK, STREAM_RESPONSE_BODY);
+
+ final URI requestUri = exchange.getRequestURI();
+ final boolean verboseRequested = getVerboseRequested(requestUri);
+
+ final DiagnosticsFactory diagnosticsFactory = server.getDiagnosticsFactory();
+ final DiagnosticsDump diagnosticsDump = diagnosticsFactory.create(verboseRequested);
+ try (OutputStream responseBody = exchange.getResponseBody()) {
+ diagnosticsDump.writeTo(responseBody);
+ }
+ } else {
+ exchange.sendResponseHeaders(HTTP_BAD_METHOD, NO_RESPONSE_BODY);
+ }
+ }
+
+ private boolean getVerboseRequested(final URI requestUri) {
+ final boolean verboseRequested;
+
+ final String query = requestUri.getQuery();
+ if (query == null) {
+ verboseRequested = false;
+ } else {
+ verboseRequested = query.contains(VERBOSE_QUERY_ENABLED);
+ }
+
+ return verboseRequested;
+ }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthHttpHandler.java b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthHttpHandler.java
new file mode 100644
index 000000000000..f37e980fa928
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthHttpHandler.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.runtime;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+import static java.net.HttpURLConnection.HTTP_OK;
+
+/**
+ * HTTP Handler for Health status operations
+ */
+class HealthHttpHandler implements HttpHandler {
+ private static final String CONTENT_TYPE_HEADER = "Content-Type";
+
+ private static final String TEXT_PLAIN = "text/plain";
+
+ private static final int NO_RESPONSE_BODY = -1;
+
+ private static final String GET_METHOD = "GET";
+
+ private static final String STATUS_UP = "Status: UP\n";
+
+ @Override
+ public void handle(final HttpExchange exchange) throws IOException {
+ final String requestMethod = exchange.getRequestMethod();
+
+ final OutputStream responseBody = exchange.getResponseBody();
+
+ if (GET_METHOD.contentEquals(requestMethod)) {
+ exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, TEXT_PLAIN);
+ final byte[] response = STATUS_UP.getBytes(StandardCharsets.UTF_8);
+ exchange.sendResponseHeaders(HTTP_OK, response.length);
+ responseBody.write(response);
+ } else {
+ exchange.sendResponseHeaders(HTTP_BAD_METHOD, NO_RESPONSE_BODY);
+ }
+ }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthStatusHistoryHttpHandler.java b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthStatusHistoryHttpHandler.java
new file mode 100644
index 000000000000..6483e0d92bc7
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/HealthStatusHistoryHttpHandler.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.runtime;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import org.apache.nifi.NiFiServer;
+import org.apache.nifi.controller.status.history.StatusHistoryDump;
+import org.apache.nifi.controller.status.history.StatusHistoryDumpFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+import static java.net.HttpURLConnection.HTTP_OK;
+
+/**
+ * HTTP Handler for Health Status History operations
+ */
+class HealthStatusHistoryHttpHandler implements HttpHandler {
+ private static final String CONTENT_TYPE_HEADER = "Content-Type";
+
+ private static final String APPLICATION_JSON = "application/json";
+
+ private static final int STREAM_RESPONSE_BODY = 0;
+
+ private static final int NO_RESPONSE_BODY = -1;
+
+ private static final String GET_METHOD = "GET";
+
+ private static final Pattern DAYS_QUERY_PATTERN = Pattern.compile("^days=(\\d+)$");
+
+ private static final int DAYS_GROUP = 1;
+
+ private static final int DAYS_DEFAULT = 1;
+
+ private final NiFiServer server;
+
+ HealthStatusHistoryHttpHandler(final NiFiServer server) {
+ this.server = Objects.requireNonNull(server);
+ }
+
+ @Override
+ public void handle(final HttpExchange exchange) throws IOException {
+ final String requestMethod = exchange.getRequestMethod();
+
+ if (GET_METHOD.contentEquals(requestMethod)) {
+ exchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, APPLICATION_JSON);
+ exchange.sendResponseHeaders(HTTP_OK, STREAM_RESPONSE_BODY);
+
+ final URI requestUri = exchange.getRequestURI();
+ final int daysRequested = getDaysRequested(requestUri);
+
+ final StatusHistoryDumpFactory statusHistoryDumpFactory = server.getStatusHistoryDumpFactory();
+ final StatusHistoryDump statusHistoryDump = statusHistoryDumpFactory.create(daysRequested);
+
+ try (OutputStream responseBody = exchange.getResponseBody()) {
+ statusHistoryDump.writeTo(responseBody);
+ }
+ } else {
+ exchange.sendResponseHeaders(HTTP_BAD_METHOD, NO_RESPONSE_BODY);
+ }
+ }
+
+ private int getDaysRequested(final URI requestUri) {
+ final int daysRequested;
+
+ final String query = requestUri.getQuery();
+ if (query == null) {
+ daysRequested = DAYS_DEFAULT;
+ } else {
+ final Matcher matcher = DAYS_QUERY_PATTERN.matcher(query);
+ if (matcher.matches()) {
+ final String daysGroup = matcher.group(DAYS_GROUP);
+ daysRequested = Integer.parseInt(daysGroup);
+ } else {
+ daysRequested = DAYS_DEFAULT;
+ }
+ }
+
+ return daysRequested;
+ }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/ManagementServer.java b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/ManagementServer.java
new file mode 100644
index 000000000000..6ecf1e5d9671
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/ManagementServer.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.runtime;
+
+/**
+ * Abstraction for controlling Management Server operation
+ */
+public interface ManagementServer {
+ /**
+ * Start Server and bind to configured address
+ */
+ void start();
+
+ /**
+ * Stop Server
+ */
+ void stop();
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/StandardManagementServer.java b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/StandardManagementServer.java
new file mode 100644
index 000000000000..01966da5427c
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/main/java/org/apache/nifi/runtime/StandardManagementServer.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.runtime;
+
+import com.sun.net.httpserver.HttpServer;
+import org.apache.nifi.NiFiServer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.InetSocketAddress;
+import java.util.Objects;
+
+/**
+ * Standard Management Server based on Java HttpServer
+ */
+public class StandardManagementServer implements ManagementServer {
+
+ private static final Logger logger = LoggerFactory.getLogger(StandardManagementServer.class);
+
+ private static final String HEALTH_PATH = "/health";
+
+ private static final String HEALTH_CLUSTER_PATH = "/health/cluster";
+
+ private static final String HEALTH_DIAGNOSTICS_PATH = "/health/diagnostics";
+
+ private static final String HEALTH_STATUS_HISTORY_PATH = "/health/status-history";
+
+ private static final int STOP_DELAY = 0;
+
+ private static final int CONNECTION_BACKLOG = 10;
+
+ private final InetSocketAddress bindAddress;
+
+ private final NiFiServer server;
+
+ private HttpServer httpServer;
+
+ public StandardManagementServer(final InetSocketAddress bindAddress, final NiFiServer server) {
+ this.bindAddress = Objects.requireNonNull(bindAddress, "Bind Address required");
+ this.server = Objects.requireNonNull(server, "Server required");
+ }
+
+ @Override
+ public void start() {
+ if (httpServer == null) {
+ try {
+ httpServer = HttpServer.create();
+
+ httpServer.createContext(HEALTH_PATH, new HealthHttpHandler());
+ httpServer.createContext(HEALTH_CLUSTER_PATH, new HealthClusterHttpHandler(server));
+ httpServer.createContext(HEALTH_DIAGNOSTICS_PATH, new HealthDiagnosticsHttpHandler(server));
+ httpServer.createContext(HEALTH_STATUS_HISTORY_PATH, new HealthStatusHistoryHttpHandler(server));
+
+ httpServer.bind(bindAddress, CONNECTION_BACKLOG);
+ httpServer.start();
+
+ final InetSocketAddress serverAddress = getServerAddress();
+
+ logger.info("Started Management Server on http://{}:{}", serverAddress.getHostString(), serverAddress.getPort());
+ } catch (final IOException e) {
+ throw new UncheckedIOException("Management Server start failed", e);
+ }
+ } else {
+ throw new IllegalStateException("Management Server running");
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (httpServer == null) {
+ logger.info("Management Server not running");
+ } else {
+ httpServer.stop(STOP_DELAY);
+ logger.info("Management Server stopped");
+ }
+ }
+
+ protected InetSocketAddress getServerAddress() {
+ return httpServer == null ? bindAddress : httpServer.getAddress();
+ }
+}
diff --git a/nifi-framework-bundle/nifi-framework/nifi-runtime/src/test/java/org/apache/nifi/runtime/StandardManagementServerTest.java b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/test/java/org/apache/nifi/runtime/StandardManagementServerTest.java
new file mode 100644
index 000000000000..1a6711338da3
--- /dev/null
+++ b/nifi-framework-bundle/nifi-framework/nifi-runtime/src/test/java/org/apache/nifi/runtime/StandardManagementServerTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.runtime;
+
+import org.apache.nifi.NiFiServer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
+@ExtendWith(MockitoExtension.class)
+class StandardManagementServerTest {
+ private static final String LOCALHOST = "127.0.0.1";
+
+ private static final String HEALTH_URI = "http://%s:%d/health";
+
+ private static final String GET_METHOD = "GET";
+
+ private static final String DELETE_METHOD = "DELETE";
+
+ private static final Duration TIMEOUT = Duration.ofSeconds(15);
+
+ @Mock
+ private NiFiServer server;
+
+ @Test
+ void testStartStop() {
+ final InetSocketAddress initialBindAddress = new InetSocketAddress(LOCALHOST, 0);
+ final StandardManagementServer standardManagementServer = new StandardManagementServer(initialBindAddress, server);
+
+ try {
+ standardManagementServer.start();
+
+ final InetSocketAddress bindAddress = standardManagementServer.getServerAddress();
+ assertNotSame(initialBindAddress.getPort(), bindAddress.getPort());
+ } finally {
+ standardManagementServer.stop();
+ }
+ }
+
+ @Test
+ void testGetHealth() throws Exception {
+ final InetSocketAddress initialBindAddress = new InetSocketAddress(LOCALHOST, 0);
+ final StandardManagementServer standardManagementServer = new StandardManagementServer(initialBindAddress, server);
+
+ try {
+ standardManagementServer.start();
+
+ final InetSocketAddress serverAddress = standardManagementServer.getServerAddress();
+ assertNotSame(initialBindAddress.getPort(), serverAddress.getPort());
+
+ assertResponseStatusCode(serverAddress, GET_METHOD, HttpURLConnection.HTTP_OK);
+ } finally {
+ standardManagementServer.stop();
+ }
+ }
+
+ @Test
+ void testDeleteHealth() throws Exception {
+ final InetSocketAddress initialBindAddress = new InetSocketAddress(LOCALHOST, 0);
+
+ final StandardManagementServer standardManagementServer = new StandardManagementServer(initialBindAddress, server);
+
+ try {
+ standardManagementServer.start();
+
+ final InetSocketAddress serverAddress = standardManagementServer.getServerAddress();
+ assertNotSame(initialBindAddress.getPort(), serverAddress.getPort());
+
+ assertResponseStatusCode(serverAddress, DELETE_METHOD, HttpURLConnection.HTTP_BAD_METHOD);
+ } finally {
+ standardManagementServer.stop();
+ }
+ }
+
+ private void assertResponseStatusCode(final InetSocketAddress serverAddress, final String method, final int responseStatusCode) throws IOException, InterruptedException {
+ final URI healthUri = URI.create(HEALTH_URI.formatted(serverAddress.getHostString(), serverAddress.getPort()));
+
+ try (HttpClient httpClient = HttpClient.newBuilder().connectTimeout(TIMEOUT).build()) {
+ final HttpRequest request = HttpRequest.newBuilder(healthUri)
+ .method(method, HttpRequest.BodyPublishers.noBody())
+ .timeout(TIMEOUT)
+ .build();
+
+ final HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ final int statusCode = response.statusCode();
+
+ assertEquals(responseStatusCode, statusCode);
+
+ final String responseBody = response.body();
+ assertNotNull(responseBody);
+ }
+ }
+}
diff --git a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/SpawnedStandaloneNiFiInstanceFactory.java b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/SpawnedStandaloneNiFiInstanceFactory.java
index 0ab74e03db34..db1565ccd028 100644
--- a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/SpawnedStandaloneNiFiInstanceFactory.java
+++ b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/SpawnedStandaloneNiFiInstanceFactory.java
@@ -16,7 +16,12 @@
*/
package org.apache.nifi.tests.system;
-import org.apache.nifi.bootstrap.RunNiFi;
+import org.apache.nifi.bootstrap.command.process.ManagementServerAddressProvider;
+import org.apache.nifi.bootstrap.command.process.ProcessBuilderProvider;
+import org.apache.nifi.bootstrap.command.process.StandardManagementServerAddressProvider;
+import org.apache.nifi.bootstrap.command.process.StandardProcessBuilderProvider;
+import org.apache.nifi.bootstrap.configuration.ConfigurationProvider;
+import org.apache.nifi.bootstrap.configuration.StandardConfigurationProvider;
import org.apache.nifi.registry.security.util.KeystoreType;
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClient;
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientConfig;
@@ -51,7 +56,7 @@ public SpawnedStandaloneNiFiInstanceFactory(final InstanceConfiguration instance
@Override
public NiFiInstance createInstance() {
- return new RunNiFiInstance(instanceConfig);
+ return new ProcessNiFiInstance(instanceConfig);
}
@Override
@@ -78,14 +83,14 @@ public int hashCode() {
return Objects.hash(instanceConfig);
}
- private static class RunNiFiInstance implements NiFiInstance {
+ private static class ProcessNiFiInstance implements NiFiInstance {
private final File instanceDirectory;
private final File configDir;
private final InstanceConfiguration instanceConfiguration;
private File bootstrapConfigFile;
- private RunNiFi runNiFi;
+ private Process process;
- public RunNiFiInstance(final InstanceConfiguration instanceConfiguration) {
+ public ProcessNiFiInstance(final InstanceConfiguration instanceConfiguration) {
this.instanceDirectory = instanceConfiguration.getInstanceDirectory();
this.bootstrapConfigFile = instanceConfiguration.getBootstrapConfigFile();
this.instanceConfiguration = instanceConfiguration;
@@ -108,30 +113,33 @@ public RunNiFiInstance(final InstanceConfiguration instanceConfiguration) {
@Override
public String toString() {
- return "RunNiFiInstance[dir=" + instanceDirectory + "]";
+ return "ProcessNiFiInstance[home=%s,process=%s]".formatted(instanceDirectory, process);
}
@Override
public void start(final boolean waitForCompletion) {
- if (runNiFi != null) {
+ if (process != null) {
throw new IllegalStateException("NiFi has already been started");
}
logger.info("Starting NiFi [{}]", instanceDirectory.getName());
- try {
- this.runNiFi = new RunNiFi(bootstrapConfigFile);
- } catch (IOException e) {
- throw new RuntimeException("Failed to start NiFi", e);
- }
+ final Map environmentVariables = Map.of("NIFI_HOME", instanceDirectory.getAbsolutePath());
+ final ConfigurationProvider configurationProvider = new StandardConfigurationProvider(environmentVariables, new Properties());
+ final ManagementServerAddressProvider managementServerAddressProvider = new StandardManagementServerAddressProvider(configurationProvider);
+ final ProcessBuilderProvider processBuilderProvider = new StandardProcessBuilderProvider(configurationProvider, managementServerAddressProvider);
try {
- runNiFi.start(false);
+ final ProcessBuilder processBuilder = processBuilderProvider.getApplicationProcessBuilder();
+ processBuilder.directory(instanceDirectory);
+ process = processBuilder.start();
+
+ logger.info("Started NiFi [{}] PID [{}]", instanceDirectory.getName(), process.pid());
if (waitForCompletion) {
waitForStartup();
}
- } catch (IOException e) {
+ } catch (final IOException e) {
throw new RuntimeException("Failed to start NiFi", e);
}
}
@@ -225,7 +233,7 @@ private void copyContents(final File dir, final File destinationDir) throws IOEx
@Override
public boolean isAccessible() {
- if (runNiFi == null) {
+ if (process == null) {
return false;
}
@@ -263,20 +271,27 @@ private void waitForStartup() throws IOException {
@Override
public void stop() {
- if (runNiFi == null) {
+ if (process == null) {
logger.info("NiFi Shutdown Ignored (runNiFi==null) [{}]", instanceDirectory.getName());
return;
}
- logger.info("NiFi Shutdown Started [{}]", instanceDirectory.getName());
+ logger.info("NiFi Process [{}] Shutdown Started [{}]", process.pid(), instanceDirectory.getName());
try {
- runNiFi.stop();
- logger.info("NiFi Shutdown Completed [{}]", instanceDirectory.getName());
- } catch (IOException e) {
+ process.destroy();
+ logger.info("NiFi Process [{}] Shutdown Requested [{}]", process.pid(), instanceDirectory.getName());
+ process.waitFor(15, TimeUnit.SECONDS);
+ logger.info("NiFi Process [{}] Shutdown Completed [{}]", process.pid(), instanceDirectory.getName());
+ } catch (final Exception e) {
throw new RuntimeException("Failed to stop NiFi", e);
} finally {
- runNiFi = null;
+ try {
+ process.destroyForcibly();
+ } catch (final Exception e) {
+ logger.warn("NiFi Process [{}] force termination failed", process.pid(), e);
+ }
+ process = null;
}
}
@@ -352,11 +367,8 @@ public void quarantineTroubleshootingInfo(final File destinationDir, final Throw
copyContents(new File(getInstanceDirectory(), dirToCopy), new File(destinationDir, dirToCopy));
}
- if (runNiFi == null) {
+ if (process == null) {
logger.warn("NiFi instance is not running so will not capture diagnostics for {}", getInstanceDirectory());
- } else {
- final File diagnosticsFile = new File(destinationDir, "diagnostics.txt");
- runNiFi.diagnostics(diagnosticsFile, false);
}
final File causeFile = new File(destinationDir, "test-failure-stack-trace.txt");
diff --git a/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/clustered/node1/logback.xml b/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/clustered/node1/logback.xml
index e29ca67b3a9d..e2d5b11a1dac 100644
--- a/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/clustered/node1/logback.xml
+++ b/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/clustered/node1/logback.xml
@@ -15,8 +15,6 @@
-->
-
-
true
diff --git a/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/clustered/node2/logback.xml b/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/clustered/node2/logback.xml
index e29ca67b3a9d..e2d5b11a1dac 100644
--- a/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/clustered/node2/logback.xml
+++ b/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/clustered/node2/logback.xml
@@ -15,8 +15,6 @@
-->
-
-
true
diff --git a/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/default/logback.xml b/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/default/logback.xml
index aa8e72d4522e..28e7c63f7ddc 100644
--- a/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/default/logback.xml
+++ b/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/default/logback.xml
@@ -15,8 +15,6 @@
-->
-
-
true
diff --git a/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/pythonic/logback.xml b/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/pythonic/logback.xml
index a3047818dd57..d387a430b82f 100644
--- a/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/pythonic/logback.xml
+++ b/nifi-system-tests/nifi-system-test-suite/src/test/resources/conf/pythonic/logback.xml
@@ -15,8 +15,6 @@
-->
-
-
true
diff --git a/pom.xml b/pom.xml
index f248106962b5..7e1e33b0ccea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -129,7 +129,7 @@
4.4.16
1.78.1
1.20.1
- 2.0.15
+ 2.0.16
2.9.0
10.17.1.0
12.0.12