/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.agent;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import mazz.i18n.Logger;
import org.rhq.core.system.ProcessExecution;
import org.rhq.core.system.ProcessExecutionResults;
import org.rhq.core.system.SystemInfo;
import org.rhq.core.system.SystemInfoFactory;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.enterprise.agent.i18n.AgentI18NFactory;
import org.rhq.enterprise.agent.i18n.AgentI18NResourceKeys;
/**
* This is a thread that will attempt to update the agent to the latest
* version. This will shutdown the currently running agent, so it is
* "destructive" in the sense that if successful, the VM this thread is running
* in will eventually die.
*
* @author John Mazzitelli
*/
public class AgentUpdateThread extends Thread {
private static final String THREAD_NAME = "RHQ Agent Update Thread";
private final Logger log = AgentI18NFactory.getLogger(AgentUpdateThread.class);
private final AgentMain agent;
private AgentPrintWriter console;
private static AtomicBoolean updating = new AtomicBoolean(false);
/**
* This static method will immediately begin to update the agent.
* Once you call this, there is no turning back - if all goes well,
* the currently running agent (and the VM its running in) will soon exit.
*
* @param agent the running agent that is to be updated
* @param wait if <code>true</code>, this will wait for the update thread
* to die. Note that if the agent update is successful, and you pass
* wait=<code>true</code>, this method will never return. It will only
* return if the update failed and the VM is still alive. Pass
* <code>false</code> if you want to fire-and-forget the agent update
* thread and return immediately.
* @throws IllegalStateException if the agent is already being updated
* @throws UnsupportedOperationException if the agent is not allowed to update itself
*/
public static void updateAgentNow(AgentMain agent, boolean wait) throws IllegalStateException {
if (!agent.getConfiguration().isAgentUpdateEnabled()) {
throw new UnsupportedOperationException(agent.getI18NMsg().getMsg(
AgentI18NResourceKeys.UPDATE_DOWNLOAD_DISABLED_BY_AGENT));
}
lock(agent); // throws exception if we are already updating the agent
Thread updateThread;
try {
updateThread = new AgentUpdateThread(agent);
updateThread.start();
} catch (Throwable t) {
unlock(); // if for any reason we can't start it, unlock us so we can attempt later
updateThread = null;
}
if (wait && updateThread != null) {
while (updateThread.isAlive()) {
try {
updateThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
Thread.currentThread().interrupt(); // interrupt the current thread so we help it exit faster
}
return;
}
/**
* This returns <code>true</code> if the agent is currently in the process of performing an update.
* When this returns <code>true</code>, it should be assumed the agent's VM will die shortly.
*
* @return <code>true</code> if an update thread is running and the update is being performed
*/
public static boolean isUpdatingNow() {
return updating.get();
}
/**
* The constructor for the thread object. This is private, go through the
* static factory method to instantiate (and start) the thread.
*
* @param agent
*/
private AgentUpdateThread(AgentMain agent) {
super(THREAD_NAME);
setDaemon(false); // can't be daemon because this thread is going to be the last one alive in this VM shortly
this.agent = agent;
this.console = agent.getOut();
}
@Override
public void run() {
AgentShutdownHook shutdownHook = new AgentShutdownHook(this.agent);
int attempts = 0;
boolean tryAgain = true;
while (tryAgain && !isInterrupted()) {
try {
showMessage(AgentI18NResourceKeys.UPDATE_THREAD_STARTED);
if (this.agent.isStarted()) {
this.agent.shutdown();
}
// let's wait for the agent's threads to fully shutdown
// (sometimes, the JBoss/Remoting threads take a long time to die)
int numThreadsStillAlive = shutdownHook.waitForNonDaemonThreads();
// get the agent update binary
AgentUpdateDownload aud;
try {
aud = new AgentUpdateDownload(this.agent);
aud.download();
aud.validate();
showMessage(AgentI18NResourceKeys.UPDATE_DOWNLOADED, aud.getAgentUpdateBinaryFile());
} catch (Exception e) {
showErrorMessage(AgentI18NResourceKeys.UPDATE_DOWNLOAD_FAILED, e.getMessage());
throw e;
}
// spawn a new Java VM to run the update jar
// if threads still aren't dead yet, make sure we pause the update longer than our kill thread wait time
String javaExe = findJavaExe();
List<String> args = new ArrayList<String>();
// On windows extra time is needed to shut down the wrapper and for windows to release file locks,
// add another 100000ms (1' 40") to the pause prior to update to help avoid locking issues.
boolean isWindows = (File.separatorChar == '\\');
String alivePause = (isWindows) ? "180000" : "80000";
String pause = (isWindows) ? "120000" : "20000";
args.add("-jar");
args.add(aud.getAgentUpdateBinaryFile().getAbsolutePath());
args.add("--pause=" + ((numThreadsStillAlive > 0) ? alivePause : pause));
args.add("--update=" + this.agent.getAgentHomeDirectory());
SystemInfo sysInfo = SystemInfoFactory.createSystemInfo();
ProcessExecution processExecution = new ProcessExecution(javaExe);
processExecution.setArguments(args);
//processExecution.setEnvironmentVariables(envvars);
processExecution.setWorkingDirectory(new File(this.agent.getAgentHomeDirectory()).getParent());
processExecution.setCaptureOutput(false);
processExecution.setWaitForCompletion(0);
showMessage(AgentI18NResourceKeys.UPDATE_THREAD_EXECUTING_UPDATE_PROCESS, processExecution);
ProcessExecutionResults results = sysInfo.executeProcess(processExecution);
if (results.getError() != null) {
throw results.getError();
}
// update has started! if this agent is running in non-daemon mode, kill
// the input stream so the input thread knows to shutdown now
try {
AgentInputReader in = this.agent.getIn();
if (in != null) {
System.in.close(); // we must ensure we close this directly!
in.close();
}
} catch (Throwable t) {
} finally {
tryAgain = false;
}
} catch (Throwable t) {
showErrorMessage(AgentI18NResourceKeys.UPDATE_THREAD_EXCEPTION, ThrowableUtil.getAllMessages(t));
// after every 5 attempts, dump a message to say we need help from an admin
attempts++;
if ((attempts % 5) == 0) {
showFinalFailureMessage(attempts);
}
// Something bad is happening - most likely we can't download the agent update binary from the server.
// Let's wait a bit longer before retrying - give the server some time to correct itself.
final long pause = 60000L;
showErrorMessage(AgentI18NResourceKeys.UPDATE_THREAD_CANNOT_RESTART_RETRY, pause);
try {
Thread.sleep(pause);
} catch (InterruptedException e) {
// our thread was interrupted
break;
}
}
}
// We should only ever get here if everything was successful.
// We now need to exit the thread; and if everything goes according to plan, the VM will now exit
shutdownHook.spawnKillThread(this.agent.getConfiguration().getAgentUpdateExitTimeout()); // pull the pin - FIRE IN THE HOLE!
return;
}
/**
* Tries to find the Java executable that launched this agent so we can use it to launch
* the agent update binary.
*
* @return the path to the Java executable
*
* @throws Exception if the Java executable could not be found
*/
private String findJavaExe() throws Exception {
// try the expected env var
String envName = "RHQ_JAVA_EXE_FILE_PATH";
String envString = System.getenv(envName);
if (envString != null) {
showMessage(AgentI18NResourceKeys.UPDATE_THREAD_USING_JAVA_EXE, envString);
return envString;
}
// try the legacy env var name for back compat
envName = "RHQ_AGENT_JAVA_EXE_FILE_PATH";
envString = System.getenv(envName);
if (envString != null) {
showMessage(AgentI18NResourceKeys.UPDATE_THREAD_USING_JAVA_EXE, envString);
return envString;
}
// one of the above RHQ environment variables should always be there. But in the odd case where it isn't
// let's try to guess where we can find the Java executable using another method
showMessage(AgentI18NResourceKeys.UPDATE_THREAD_LOOKING_FOR_JAVA_EXE, envName);
String javaExe = "java"; // fallback to this if we can't find it - let's hope its in our path
String javaHome = System.getProperty("java.home");
if (javaHome != null) {
File[] guesses = new File[] { new File(javaHome, "bin/java"), new File(javaHome, "bin/java.exe"),
new File(javaHome, "java"), new File(javaHome, "java.exe") };
for (File guess : guesses) {
if (guess.isFile()) {
javaExe = guess.getAbsolutePath();
break;
}
}
}
showMessage(AgentI18NResourceKeys.UPDATE_THREAD_USING_JAVA_EXE, javaExe);
return javaExe;
}
/**
* This sets the flag that ensures only one update thread is ever created at any one time.
* If there is already an agent update thread created, this throws an exception.
*
* @param agent the agent that is to be updated
* @throws IllegalStateException if the agent is already updating itself
*/
private static void lock(AgentMain agent) throws IllegalStateException {
if (!updating.compareAndSet(false, true)) {
throw new IllegalStateException(agent.getI18NMsg().getMsg(AgentI18NResourceKeys.UPDATE_THREAD_DUP));
}
}
/**
* This sets the flag to allow another update thread to be created.
*/
private static void unlock() {
updating.set(false);
}
/**
* Because this thread is performing very important and serious things, we will
* both log the message and output it to the console, to give the user ample
* notification of what is going on.
*
* @param msg
* @param args
*/
private void showMessage(String msg, Object... args) {
log.info(msg, args);
this.console.println(this.agent.getI18NMsg().getMsg(msg, args));
}
/**
* Because this thread is performing very important and serious things, we will
* both log the error message and output it to the console, to give the user ample
* notification of what is going on.
*
* @param msg
* @param args
*/
private void showErrorMessage(String msg, Object... args) {
try {
// log at fatal level because if we can't update, it probably means the agent is dead in the water
// and will never be able to talk to the server again - manual admin intervention is probably required now
log.fatal(msg, args);
this.console.println(this.agent.getI18NMsg().getMsg(msg, args));
} catch (Throwable t) {
}
}
/**
* This will also log a generic failure message to tell the user that the agent
* is in a really bad state now and manual intervention by an administrator is
* probably needed.
*
* @param attempts number of times the update was tried
*/
private void showFinalFailureMessage(int attempts) {
try {
log.fatal(AgentI18NResourceKeys.UPDATE_THREAD_FAILURE, attempts);
this.console.println(this.agent.getI18NMsg().getMsg(AgentI18NResourceKeys.UPDATE_THREAD_FAILURE, attempts));
} catch (Throwable t) {
}
}
}