/*
* JBoss, Home of Professional Open Source.
* Copyright 2010, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.process;
import static java.lang.Thread.holdsLock;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.jboss.as.process.logging.ProcessLogger;
import org.jboss.as.process.protocol.StreamUtils;
import org.jboss.as.process.stdin.Base64OutputStream;
import org.jboss.logging.Logger;
import org.wildfly.common.Assert;
/**
* A managed process.
*
* @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
* @author <a href="mailto:kabir.khan@jboss.com">Kabir Khan</a>
* @author Emanuel Muckenhuber
*/
final class ManagedProcess {
private final String processName;
private final List<String> command;
private final Map<String, String> env;
private final String workingDirectory;
private final ProcessLogger log;
private final Object lock;
private final ProcessController processController;
private final String authKey;
private final boolean isPrivileged;
private final RespawnPolicy respawnPolicy;
private OutputStream stdin;
private volatile State state = State.DOWN;
private volatile Thread joinThread;
private Process process;
private boolean shutdown;
private boolean stopRequested = false;
private final AtomicInteger respawnCount = new AtomicInteger(0);
public String getAuthKey() {
return authKey;
}
public boolean isPrivileged() {
return isPrivileged;
}
public boolean isRunning() {
return (state == State.STARTED) || (state == State.STOPPING);
}
public boolean isStopping() {
return state == State.STOPPING;
}
enum State {
DOWN,
STARTED,
STOPPING,
;
}
ManagedProcess(final String processName, final List<String> command, final Map<String, String> env, final String workingDirectory, final Object lock, final ProcessController controller, final String authKey, final boolean privileged, final boolean respawn) {
Assert.checkNotNullParam("processName", processName);
Assert.checkNotNullParam("command", command);
Assert.checkNotNullParam("env", env);
Assert.checkNotNullParam("workingDirectory", workingDirectory);
Assert.checkNotNullParam("lock", lock);
Assert.checkNotNullParam("controller", controller);
Assert.checkNotNullParam("authKey", authKey);
if (authKey.length() != ProcessController.AUTH_BYTES_ENCODED_LENGTH) {
throw ProcessLogger.ROOT_LOGGER.invalidLength("authKey");
}
this.processName = processName;
this.command = command;
this.env = env;
this.workingDirectory = workingDirectory;
this.lock = lock;
processController = controller;
this.authKey = authKey;
isPrivileged = privileged;
respawnPolicy = respawn ? RespawnPolicy.RESPAWN : RespawnPolicy.NONE;
log = Logger.getMessageLogger(ProcessLogger.class, "org.jboss.as.process." + processName + ".status");
}
int incrementAndGetRespawnCount() {
return respawnCount.incrementAndGet();
}
int resetRespawnCount() {
return respawnCount.getAndSet(0);
}
public String getProcessName() {
return processName;
}
public void start() {
synchronized (lock) {
if (state != State.DOWN) {
log.debugf("Attempted to start already-running process '%s'", processName);
return;
}
resetRespawnCount();
doStart(false);
}
}
public void sendStdin(final InputStream msg) throws IOException {
assert holdsLock(lock); // Call under lock
try {
// WFLY-2697 All writing is in Base64
Base64OutputStream base64 = getBase64OutputStream(stdin);
StreamUtils.copyStream(msg, base64);
base64.close(); // not flush(). close() writes extra data to the stream allowing Base64 input stream
// to distinguish end of message
} catch (IOException e) {
log.failedToSendDataBytes(e, processName);
throw e;
}
}
public void reconnect(String scheme, String hostName, int port, boolean managementSubsystemEndpoint, String asAuthKey) {
assert holdsLock(lock); // Call under lock
try {
// WFLY-2697 All writing is in Base64
Base64OutputStream base64 = getBase64OutputStream(stdin);
StreamUtils.writeUTFZBytes(base64, scheme);
StreamUtils.writeUTFZBytes(base64, hostName);
StreamUtils.writeInt(base64, port);
StreamUtils.writeBoolean(base64, managementSubsystemEndpoint);
base64.write(asAuthKey.getBytes(Charset.forName("US-ASCII")));
base64.close(); // not flush(). close() writes extra data to the stream allowing Base64 input stream
// to distinguish end of message
} catch (IOException e) {
if(state == State.STARTED) {
// Only log in case the process is still running
log.failedToSendReconnect(e, processName);
}
}
}
void doStart(boolean restart) {
// Call under lock
assert holdsLock(lock);
stopRequested = false;
final List<String> command = new ArrayList<String>(this.command);
if(restart) {
//Add the restart flag to the HC process if we are respawning it
command.add(CommandLineConstants.PROCESS_RESTARTED);
}
log.startingProcess(processName);
log.debugf("Process name='%s' command='%s' workingDirectory='%s'", processName, command, workingDirectory);
final ProcessBuilder builder = new ProcessBuilder(command.stream().map(c -> c.trim()).collect(Collectors.toList()));
builder.environment().putAll(env);
builder.directory(new File(workingDirectory));
final Process process;
try {
process = builder.start();
} catch (IOException e) {
e.printStackTrace();
processController.operationFailed(processName, ProcessMessageHandler.OperationType.START);
log.failedToStartProcess(processName);
return;
}
final long startTime = System.currentTimeMillis();
final OutputStream stdin = process.getOutputStream();
final InputStream stderr = process.getErrorStream();
final InputStream stdout = process.getInputStream();
final Thread stderrThread = new Thread(new ReadTask(stderr, processController.getStderr()));
stderrThread.setName(String.format("stderr for %s", processName));
stderrThread.start();
final Thread stdoutThread = new Thread(new ReadTask(stdout, processController.getStdout()));
stdoutThread.setName(String.format("stdout for %s", processName));
stdoutThread.start();
joinThread = new Thread(new JoinTask(startTime));
joinThread.setName(String.format("reaper for %s", processName));
joinThread.start();
boolean ok = false;
try {
// WFLY-2697 All writing is in Base64
OutputStream base64 = getBase64OutputStream(stdin);
base64.write(authKey.getBytes(Charset.forName("US-ASCII")));
base64.close(); // not flush(). close() writes extra data to the stream allowing Base64 input stream
// to distinguish end of message
ok = true;
} catch (Exception e) {
log.failedToSendAuthKey(processName, e);
}
this.process = process;
this.stdin = stdin;
if(ok) {
state = State.STARTED;
processController.processStarted(processName);
} else {
processController.operationFailed(processName, ProcessMessageHandler.OperationType.START);
}
return;
}
public void stop() {
synchronized (lock) {
if (state != State.STARTED) {
log.debugf("Attempted to stop already-stopping or down process '%s'", processName);
return;
}
log.stoppingProcess(processName);
stopRequested = true;
StreamUtils.safeClose(stdin);
state = State.STOPPING;
}
}
public void destroy() {
synchronized (lock) {
Thread jt = joinThread;
if(state != State.STOPPING) {
stop(); // Try to stop before destroying the process
}
if (state != State.DOWN && jt != null) {
try {
// Give stop() a small amount of time to work,
// in case the user asked for a destroy when a normal stop
// was sufficient. But the base assumption is the destroy
// is needed
jt.join(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (state != State.DOWN || jt == null || jt.isAlive()) { // Cover all bases just to be robust
log.debugf("Destroying process '%s'", processName);
process.destroyForcibly();
}
}
}
public void kill() {
synchronized (lock) {
Thread jt = joinThread;
if(state != State.STOPPING) {
stop(); // Try to stop before killing the process
}
if (state != State.DOWN && jt != null) {
try {
// Give stop() a small amount of time to work,
// in case the user asked for a kill when a normal stop
// was sufficient. But the base assumption is the kill
// is needed
jt.join(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (state != State.DOWN || jt == null || jt.isAlive()) { // Cover all bases just to be robust
log.debugf("Attempting to kill -KILL process '%s'", processName);
if (!ProcessUtils.killProcess(processName)) {
// Fallback to destroy if kill is not available
log.failedToKillProcess(processName);
process.destroyForcibly();
}
}
}
}
public void shutdown() {
synchronized (lock) {
if(shutdown) {
return;
}
shutdown = true;
if (state == State.STARTED) {
log.stoppingProcess(processName);
stopRequested = true;
StreamUtils.safeClose(stdin);
state = State.STOPPING;
} else if (state == State.STOPPING) {
return;
} else {
new Thread() {
@Override
public void run() {
processController.removeProcess(processName);
}
}.start();
}
}
}
void respawn() {
synchronized (lock) {
if (state != State.DOWN) {
log.debugf("Attempted to respawn already-running process '%s'", processName);
return;
}
doStart(true);
}
}
private static Base64OutputStream getBase64OutputStream(OutputStream toWrap) {
// We'll call close on Base64OutputStream at the end of each message
// to serve as a delimiter. Don't let that close the underlying stream.
OutputStream nonclosing = new FilterOutputStream(toWrap) {
@Override
public void close() throws IOException {
flush();
}
};
return new Base64OutputStream(nonclosing);
}
private final class JoinTask implements Runnable {
private final long startTime;
public JoinTask(final long startTime) {
this.startTime = startTime;
}
public void run() {
final Process process;
synchronized (lock) {
process = ManagedProcess.this.process;
}
int exitCode;
for (;;) try {
exitCode = process.waitFor();
log.processFinished(processName, exitCode);
break;
} catch (InterruptedException e) {
// ignore
}
boolean respawn = false;
boolean slowRespawn = false;
boolean unlimitedRespawn = false;
int respawnCount = 0;
synchronized (lock) {
final long endTime = System.currentTimeMillis();
processController.processStopped(processName, endTime - startTime);
state = State.DOWN;
if (shutdown) {
processController.removeProcess(processName);
} else if (isPrivileged() && exitCode == ExitCodes.HOST_CONTROLLER_ABORT_EXIT_CODE) {
// Host Controller abort. See if there are other running processes the HC
// needs to manage. If so we must restart the HC.
if (processController.getOngoingProcessCount() > 1) {
respawn = true;
respawnCount = ManagedProcess.this.incrementAndGetRespawnCount();
unlimitedRespawn = true;
// We already have servers, so this isn't an abort in the early stages of the
// initial HC boot. Likely it's due to a problem in a reload, which will require
// some sort of user intervention to resolve. So there is no point in immediately
// respawning and spamming the logs.
slowRespawn = true;
} else {
processController.removeProcess(processName);
new Thread(new Runnable() {
public void run() {
processController.shutdown();
System.exit(ExitCodes.NORMAL);
}
}).start();
}
} else if (isPrivileged() && exitCode == ExitCodes.RESTART_PROCESS_FROM_STARTUP_SCRIPT) {
// Host Controller restart via exit code picked up by script
processController.removeProcess(processName);
new Thread(new Runnable() {
public void run() {
processController.shutdown();
System.exit(ExitCodes.RESTART_PROCESS_FROM_STARTUP_SCRIPT);
}
}).start();
} else {
if(! stopRequested) {
respawn = true;
respawnCount = ManagedProcess.this.incrementAndGetRespawnCount();
if (isPrivileged() && processController.getOngoingProcessCount() > 1) {
// This is an HC with live servers to manage, so never give up on
// restarting
unlimitedRespawn = true;
}
}
}
stopRequested = false;
}
if(respawn) {
respawnPolicy.respawn(respawnCount, ManagedProcess.this, slowRespawn, unlimitedRespawn);
}
}
}
private final class ReadTask implements Runnable {
private final InputStream source;
private final PrintStream target;
private ReadTask(final InputStream source, final PrintStream target) {
this.source = source;
this.target = target;
}
public void run() {
final InputStream source = this.source;
final String processName = ManagedProcess.this.processName;
try {
final BufferedReader reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(source), StandardCharsets.UTF_8));
final OutputStreamWriter writer = new OutputStreamWriter(target, StandardCharsets.UTF_8);
String s;
String prevEscape = "";
while ((s = reader.readLine()) != null) {
// Has ANSI?
int i = s.lastIndexOf('\033');
int j = i != -1 ? s.indexOf('m', i) : 0;
synchronized (target) {
writer.write('[');
writer.write(processName);
writer.write("] ");
writer.write(prevEscape);
writer.write(s);
// Reset if there was ANSI
if (j != 0 || prevEscape != "") {
writer.write("\033[0m");
}
writer.write('\n');
writer.flush();
}
// Remember escape code for the next line
if (j != 0) {
String escape = s.substring(i, j + 1);
if (!"\033[0m".equals(escape)) {
prevEscape = escape;
} else {
prevEscape = "";
}
}
}
source.close();
} catch (IOException e) {
log.streamProcessingFailed(processName, e);
} finally {
StreamUtils.safeClose(source);
}
}
}
}