/* * 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.update; import gnu.getopt.Getopt; import gnu.getopt.LongOpt; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.net.URI; import java.net.URL; import java.util.Date; import java.util.Map; import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.apache.tools.ant.Project; import org.apache.tools.ant.helper.ProjectHelper2; /** * The main entry class to the standalone agent updater. * This class must be placed in the agent update binary jar file in the * proper place and its name must be specified in that jar file's Main-Class * manifest entry. * * @author John Mazzitelli * */ public class AgentUpdate { private static final String RHQ_AGENT_UPDATE_VERSION_PROPERTIES = "rhq-agent-update-version.properties"; private static final String BUILD_TASKS_PROPERTIES_FILE = "rhq-agent-update-build-tasks.properties"; private static final String ANT_TARGET_BACKUP_AGENT = "backup-agent"; private static final String ANT_TARGET_RESTORE_AGENT = "restore-agent"; private static final String ANT_TARGET_LAUNCH_AGENT = "launch-agent"; private static final String DEFAULT_OLD_AGENT_HOME = "rhq-agent"; private static final String DEFAULT_NEW_AGENT_HOME_PARENT = "."; private static final String DEFAULT_LOG_FILE = "rhq-agent-update.log"; private static final boolean DEFAULT_QUIET_FLAG = false; private static final String DEFAULT_SCRIPT_FILE = "rhq-agent-update-build.xml"; private boolean showVersion = false; private boolean updateFlag = false; private boolean installFlag = false; private Boolean launchFlag; private String oldAgentHomeArgument = DEFAULT_OLD_AGENT_HOME; private String newAgentHomeParentArgument = DEFAULT_NEW_AGENT_HOME_PARENT; private String logFileArgument = DEFAULT_LOG_FILE; private String jarFileArgument = null; private boolean quietFlag = DEFAULT_QUIET_FLAG; private String scriptFileArgument = DEFAULT_SCRIPT_FILE; /** * The main startup point for the self-executing agent update jar. * * @param args the command line arguments */ public static void main(String args[]) throws Exception { AgentUpdate agentUpdate = new AgentUpdate(); // check the command line arguments and set our internals based on them // any exceptions coming out of here will abort the update try { agentUpdate.processArguments(args); } catch (IllegalArgumentException error) { error.printStackTrace(); agentUpdate.printSyntax(); return; } catch (UnsupportedOperationException helpException) { // this exception occurs simply when the --help option was specified or help needs to be displayed try { System.out.println(new String(agentUpdate.getJarFileContent("README.txt"))); } catch (Throwable t) { System.out.println("Cannot show README.txt: " + t); } System.out.println(); agentUpdate.printSyntax(); System.out.println(); agentUpdate.showVersionInfo(); return; } // if the user passed --version, show the version and exit immediately if (agentUpdate.showVersion) { agentUpdate.showVersionInfo(); return; } // where should we log our messages? File logFile = new File(agentUpdate.logFileArgument); // what script should we use? if user did not specify a custom one, use the default // one located in our classloader; otherwise, find the custom one on file system URL buildFile; if (DEFAULT_SCRIPT_FILE.equals(agentUpdate.scriptFileArgument)) { buildFile = AgentUpdate.class.getClassLoader().getResource(agentUpdate.scriptFileArgument); } else { buildFile = new File(agentUpdate.scriptFileArgument).toURI().toURL(); } // set some properties that we can pass to the ANT script Properties props = agentUpdate.getAgentUpdateVersionProperties(); props.setProperty("rhq.agent.update.jar-file", agentUpdate.getJarFilename()); props.setProperty("rhq.agent.update.log-dir", (logFile.getParent() != null) ? logFile.getParent() : "."); if (agentUpdate.updateFlag) { props.setProperty("rhq.agent.update.update-flag", "true"); props.setProperty("rhq.agent.update.update-agent-dir", agentUpdate.oldAgentHomeArgument); props.setProperty("rhq.agent.update.launch-script-dir", new File(agentUpdate.oldAgentHomeArgument, "bin") .getAbsolutePath()); } else if (agentUpdate.installFlag) { props.setProperty("rhq.agent.update.install-flag", "true"); props.setProperty("rhq.agent.update.install-agent-dir", agentUpdate.newAgentHomeParentArgument); props.setProperty("rhq.agent.update.launch-script-dir", new File(agentUpdate.newAgentHomeParentArgument, "rhq-agent/bin").getAbsolutePath()); } // if we are updating, backup the current agent just in case we have to restore it if (agentUpdate.updateFlag) { try { agentUpdate.startAnt(buildFile, ANT_TARGET_BACKUP_AGENT, BUILD_TASKS_PROPERTIES_FILE, props, logFile, !agentUpdate.quietFlag); } catch (Exception e) { logMessage(logFile, "WARNING! Agent backup failed! Agent will not recover if it can't update!"); logStackTrace(logFile, e); } } // run the default ant script target now try { agentUpdate.startAnt(buildFile, null, BUILD_TASKS_PROPERTIES_FILE, props, logFile, !agentUpdate.quietFlag); } catch (Exception e) { // if we were updating, try to restore the old agent to recover from the error if (agentUpdate.updateFlag) { logMessage(logFile, "WARNING! Agent update failed! Will try to restore old agent!"); logStackTrace(logFile, e); try { agentUpdate.startAnt(buildFile, ANT_TARGET_RESTORE_AGENT, BUILD_TASKS_PROPERTIES_FILE, props, logFile, true); } catch (Exception e2) { logMessage(logFile, "WARNING! Agent restore failed! Agent is dead and cannot recover!"); logStackTrace(logFile, e2); } } else { logMessage(logFile, "WARNING! Agent installation failed!"); logStackTrace(logFile, e); throw e; } } // if we are not to start the agent, we are done and can return now if (!agentUpdate.launchFlag.booleanValue()) { return; } // now start the agent using the proper launcher script try { agentUpdate.startAnt(buildFile, ANT_TARGET_LAUNCH_AGENT, BUILD_TASKS_PROPERTIES_FILE, props, logFile, true); } catch (Exception e) { logMessage(logFile, "WARNING! Agent failed to be restarted!"); logStackTrace(logFile, e); throw e; } return; } /** * Logs a message to both the log file and the stdout console. * * @param logFile where to write the log * @param msg the message to log */ private static void logMessage(File logFile, String msg) { msg = new Date().toString() + ": " + msg; System.out.println(msg); try { PrintWriter pw = new PrintWriter(new FileOutputStream(logFile, true)); try { pw.println(msg); } finally { pw.close(); } } catch (Throwable t) { } } /** * Logs a stack trace to both the log file and the stdout console. * * @param logFile where to write the stack track * @param t the exception whose stack track is to be logged */ private static void logStackTrace(File logFile, Throwable t) { t.printStackTrace(System.out); try { PrintWriter pw = new PrintWriter(new FileOutputStream(logFile, true)); try { t.printStackTrace(pw); } finally { pw.close(); } } catch (Throwable t1) { } } /** * Logs a message to both the log file and the stdout console. * * @param msg the message to log */ private void logMessage(String msg) { msg = new Date().toString() + ": " + msg; System.out.println(msg); try { PrintWriter pw = new PrintWriter(new FileOutputStream(logFileArgument, true)); try { pw.println(msg); } finally { pw.close(); } } catch (Throwable t) { } } private void showVersionInfo() throws Exception { String str = "\n" // + "============================================\n" // + "RHQ Agent Update Binary Version Information:\n" // + "============================================\n" // + new String(getJarFileContent(RHQ_AGENT_UPDATE_VERSION_PROPERTIES)); logMessage(str); } private String getJarFilename() throws Exception { if (this.jarFileArgument == null) { URL propsUrl = this.getClass().getClassLoader().getResource(RHQ_AGENT_UPDATE_VERSION_PROPERTIES); String propsUrlString = propsUrl.toString(); // the URL string is something like "jar:file:/dir/foo.jar!/rhq-agent-update-version.properties // we need to get the filename, so strip the jar stuff off of it propsUrlString = propsUrlString.replaceFirst("jar:", ""); propsUrlString = propsUrlString.replaceFirst("!/" + RHQ_AGENT_UPDATE_VERSION_PROPERTIES, ""); File propsFile = new File(new URI(propsUrlString)); this.jarFileArgument = propsFile.getAbsolutePath(); } return this.jarFileArgument; } private Properties getAgentUpdateVersionProperties() throws Exception { byte[] bytes = getJarFileContent(RHQ_AGENT_UPDATE_VERSION_PROPERTIES); InputStream propertiesStream = new ByteArrayInputStream(bytes); Properties versionProps = new Properties(); try { versionProps.load(propertiesStream); } finally { propertiesStream.close(); } return versionProps; } private byte[] getJarFileContent(String filename) throws Exception { JarFile jarFile = new JarFile(getJarFilename()); // use the jar file because user might have used --jar try { JarEntry jarFileEntry = jarFile.getJarEntry(filename); InputStream jarFileEntryStream = jarFile.getInputStream(jarFileEntry); return slurp(jarFileEntryStream); } finally { jarFile.close(); } } private void printSyntax() { String syntax = "Valid options are:\n" // + "[--help] : Help information on how to use this jar file.\n" // + "[--version] : Shows version information about this jar file and exits.\n" // + "[--update[=<old agent home>]] : When specified, this will update an existing\n" // + " agent. If you do not specify the directory\n" // + " where the existing agent is, the default is:\n" // + " " + DEFAULT_OLD_AGENT_HOME + "\n" // + " This is mutually exclusive of --install\n" // + "[--install[=<new agent dir>]] : When specified, this will install a new agent\n" // + " without attempting to update any existing\n" // + " agent. If you do not specify the directory,\n" // + " the default is:" + DEFAULT_NEW_AGENT_HOME_PARENT + "\n" // + " Note the directory will be the parent of the\n" // + " new agent home installation directory.\n" // + " This is mutually exclusive of --update\n" // + "[--launch=<true|false>] : If specified, this explicitly indicates if the\n" // + " agent should be started immediately after being\n" // + " installed or updated. If not specified, the\n" // + " default will be 'false' if installing a new agent\n" // + " and 'true' if updating an existing agent.\n" // + "[--quiet] : If specified, this turns off console log messages.\n" // + "[--pause[=<ms>]] : If specified, the update will not occur until the given\n" // + " number of milliseconds expires. If this option is given\n" // + " without the number of milliseconds, 30000 is the default.\n" // + "[--jar=<jar file>] : If specified, the agent found in the given jar file will\n" // + " be the new one that will be installed. You usually do not\n" // + " have to specify this, since the jar running this update\n" // + " code will usually be the one that contains the agent to\n" // + " be installed. Do not use this unless you have a reason.\n" // + "[--log=<log file>] : If specified, this is where the log messages will be\n" // + " written. Default=" + DEFAULT_LOG_FILE + "\n" // + "[--script=<ant script>] : If specified, this will override the default\n" // + " upgrade script URL found in the classloader.\n" // + " Users will rarely need this;\n" // + " use this only if you know what you are doing.\n" // + " Default=" + DEFAULT_SCRIPT_FILE + "\n"; // System.out.println(syntax); } /** * Processes the command line arguments passed to the self-executing jar. * * @param args the arguments to process * * @throws UnsupportedOperationException if the help option was specified * @throws IllegalArgumentException if an argument was invalid * @throws FileNotFoundException if --update is specified with an invalid agent home directory */ private void processArguments(String args[]) throws Exception { // if no arguments were specified, then we assume user needs help if (args.length <= 0) { throw new UnsupportedOperationException(); } String sopts = "u::i::qhvo:j:p::s:l:"; LongOpt[] lopts = { new LongOpt("update", LongOpt.OPTIONAL_ARGUMENT, null, 'u'), // updates existing agent new LongOpt("install", LongOpt.OPTIONAL_ARGUMENT, null, 'i'), // installs agent new LongOpt("quiet", LongOpt.NO_ARGUMENT, null, 'q'), // if not set, dumps log message to stdout too new LongOpt("launch", LongOpt.REQUIRED_ARGUMENT, null, 'l'), // if agent should be started or not new LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h'), // will show the syntax help new LongOpt("version", LongOpt.NO_ARGUMENT, null, 'v'), // shows version info new LongOpt("log", LongOpt.REQUIRED_ARGUMENT, null, 'o'), // location of the log file new LongOpt("jar", LongOpt.REQUIRED_ARGUMENT, null, 'j'), // location of an external jar that has our agent new LongOpt("pause", LongOpt.OPTIONAL_ARGUMENT, null, 'p'), // pause (sleep) before updating new LongOpt("script", LongOpt.REQUIRED_ARGUMENT, null, 's') }; // switch immediately to the given server Getopt getopt = new Getopt(AgentUpdate.class.getSimpleName(), args, sopts, lopts); int code; long pause = -1L; while ((code = getopt.getopt()) != -1) { switch (code) { case ':': case '?': case 1: { throw new IllegalArgumentException("Bad argument!"); } case 'u': { this.updateFlag = true; String value = getopt.getOptarg(); if (value != null) { this.oldAgentHomeArgument = value; } // make sure the directory actually exists File agentHome = new File(this.oldAgentHomeArgument); if (!agentHome.exists() || !agentHome.isDirectory()) { throw new FileNotFoundException("There is no agent located at: " + this.oldAgentHomeArgument); } // be nice for the user - if the user specified the agent's parent directory, // then set the old agent home argument to the "real" agent home directory File possibleHomeDir = new File(agentHome, "rhq-agent"); if (possibleHomeDir.exists() && possibleHomeDir.isDirectory()) { agentHome = possibleHomeDir; } // make it an absolute path this.oldAgentHomeArgument = agentHome.getAbsolutePath(); break; } case 'i': { this.installFlag = true; String value = getopt.getOptarg(); if (value != null) { this.newAgentHomeParentArgument = value; } break; } case 'q': { this.quietFlag = true; break; } case 'h': { throw new UnsupportedOperationException(); } case 'v': { this.showVersion = true; break; } case 'o': { this.logFileArgument = getopt.getOptarg(); break; } case 'l': { this.launchFlag = Boolean.valueOf(Boolean.parseBoolean(getopt.getOptarg())); break; } case 'j': { this.jarFileArgument = getopt.getOptarg(); File jarFile = new File(this.jarFileArgument); if (!jarFile.exists() || !jarFile.isFile()) { throw new FileNotFoundException("There is no agent jar located at: " + this.jarFileArgument); } break; } case 'p': { pause = 30000L; String value = getopt.getOptarg(); if (value != null) { try { pause = Long.parseLong(value); } catch (Exception e) { pause = 30000L; } } break; } case 's': { this.scriptFileArgument = getopt.getOptarg(); break; } } } if (getopt.getOptind() < args.length) { throw new IllegalArgumentException("Bad arguments."); } if (this.showVersion) { return; // do not continue, this will exit the VM after showing the version info } if (this.updateFlag && this.installFlag) { throw new IllegalArgumentException("Cannot use both --update and --install"); } if (!this.updateFlag && !this.installFlag) { throw new IllegalArgumentException("Must specify either --update or --install"); } if (this.launchFlag == null) { // default is to start agent if we are updating; not start if installing this.launchFlag = Boolean.valueOf(this.updateFlag); } if (pause > 0) { try { logMessage("Pausing for [" + pause + "] milliseconds..."); Thread.sleep(pause); } catch (InterruptedException e) { } finally { logMessage("Done pausing. Continuing with the update."); } } return; } private byte[] slurp(InputStream stream) { ByteArrayOutputStream out = new ByteArrayOutputStream(); long numBytesCopied = 0; int bufferSize = 32768; try { // make sure we buffer the input stream = new BufferedInputStream(stream, bufferSize); byte[] buffer = new byte[bufferSize]; for (int bytesRead = stream.read(buffer); bytesRead != -1; bytesRead = stream.read(buffer)) { out.write(buffer, 0, bytesRead); numBytesCopied += bytesRead; } } catch (IOException ioe) { throw new RuntimeException("Stream data cannot be slurped", ioe); } finally { try { stream.close(); } catch (IOException ioe2) { } } return out.toByteArray(); } /** * Launches ANT and runs the default target in the given build file. * * @param buildFile the build file that ANT will run * @param target the target to run, <code>null</code> for the default target * @param customTaskDefs the properties file found in classloader that contains all the taskdef definitions * @param properties set of properties to set for the ANT task to access * @param logFile where ANT messages will be logged * @param logStdOut if <code>true</code>, log messages will be sent to stdout as well as the log file * * @throws RuntimeException */ private void startAnt(URL buildFile, String target, String customTaskDefs, Properties properties, File logFile, boolean logStdOut) { PrintWriter logFileOutput = null; try { logFileOutput = new PrintWriter(new FileOutputStream(logFile, true)); ClassLoader classLoader = getClass().getClassLoader(); Properties taskDefs = new Properties(); if (customTaskDefs != null) { InputStream taskDefsStream = classLoader.getResourceAsStream(customTaskDefs); try { taskDefs.load(taskDefsStream); } finally { taskDefsStream.close(); } } Project project = new Project(); project.setCoreLoader(classLoader); project.init(); // notice we are adding the listener before we set the properties - if we do not want the // the properties echoed out in the log (e.g. if they contain sensitive passwords) // we should do this after we set the properties. project.addBuildListener(new LoggerAntBuildListener(target, logFileOutput, Project.MSG_DEBUG)); if (logStdOut) { PrintWriter stdout = new PrintWriter(System.out); project.addBuildListener(new LoggerAntBuildListener(target, stdout, Project.MSG_INFO)); } if (properties != null) { for (Map.Entry<Object, Object> property : properties.entrySet()) { project.setProperty(property.getKey().toString(), property.getValue().toString()); } } for (Map.Entry<Object, Object> taskDef : taskDefs.entrySet()) { project.addTaskDefinition(taskDef.getKey().toString(), Class.forName(taskDef.getValue().toString(), true, classLoader)); } new ProjectHelper2().parse(project, buildFile); project.executeTarget((target == null) ? project.getDefaultTarget() : target); } catch (Exception e) { throw new RuntimeException("Cannot run ANT on script [" + buildFile + "]. Cause: " + e, e); } finally { if (logFileOutput != null) { logFileOutput.close(); } } } }