/* * RHQ Management Platform * Copyright (C) 2005-2013 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.core.pluginapi.util; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.hyperic.sigar.ProcExe; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.rhq.core.system.ProcessInfo; /** * A set of utility methods that server discovery components can use to discover the values required to later restart * the server process via a start script. The values can then be stored in or retrieved from the server's plugin * configuration using the {@link StartScriptConfiguration} plugin configuration wrapper. * * @author Ian Springer */ public class ServerStartScriptDiscoveryUtility { private static final boolean OS_IS_WINDOWS = (File.separatorChar == '\\'); private static final char OPTION_PREFIX = (OS_IS_WINDOWS) ? '/' : '-'; // Generic OS-level PATH setting for LINUX. For Windows the PATH must be generated when we have // the system env vars. It will be of the form %SystemRoot%\\system32;%SystemRoot%; private static final String CORE_ENV_VAR_PATH_UNIX = "/bin:/usr/bin"; // Generic OS-level env vars that should be in every process's environment. private static final Set<String> CORE_ENV_VAR_NAME_INCLUDES = new HashSet<String>(Arrays.asList("PATH", "LD_LIBRARY_PATH")); private static final String NOHUP_PATH = "/usr/bin/nohup"; private static final String SUDO_PATH = "/usr/bin/sudo"; static { if (OS_IS_WINDOWS) { CORE_ENV_VAR_NAME_INCLUDES.add("OS"); // many batch files use this to figure out if the OS type is NT or 9x CORE_ENV_VAR_NAME_INCLUDES.add("SYSTEMROOT"); // required on Windows to avoid winsock create errors } } private ServerStartScriptDiscoveryUtility() { } /** * If the specified process is a script, return the path to the script - the returned path will be absolute and * canonical if possible, or, if it is not a script, return <code>null</code>. * * @param serverParentProcess the parent process of a server (e.g. JBoss AS) process * * @return if the specified process is a script (the returned path will be absolute and * canonical if possible), the path to the script, otherwise <code>null</code> */ @Nullable public static File getStartScript(ProcessInfo serverParentProcess) { // e.g. UNIX: "/bin/sh ./standalone.sh --server-config=standalone-full.xml" // Windows: "cmd.exe [options] standalone.bat --server-config=standalone-full.xml" String[] serverParentProcessCommandLine = serverParentProcess.getCommandLine(); Integer startScriptIndex = getStartScriptIndex(serverParentProcessCommandLine); File startScriptFile; if (startScriptIndex != null) { // The process is a script - excellent! String startScript = (serverParentProcessCommandLine.length > startScriptIndex) ? serverParentProcessCommandLine[startScriptIndex] : null; startScriptFile = new File(startScript); if (!startScriptFile.isAbsolute()) { ProcExe parentProcessExe = serverParentProcess.getExecutable(); if (parentProcessExe == null) { // TODO: This isn't really generic startScriptFile = new File("bin", startScriptFile.getName()); } else { String cwd = parentProcessExe.getCwd(); startScriptFile = new File(cwd, startScriptFile.getPath()); startScriptFile = new File(FileUtils.getCanonicalPath(startScriptFile.getPath())); } } } else { // The parent process is not a script - either the user started the server via some other mechanism, or the // script process got killed. startScriptFile = null; } return startScriptFile; } /** * Return the command line prefix that should be used when restarting the specified server process. * * @param serverProcess a server (e.g. JBoss AS) process * @param thisProcess this java process * * @return the command line prefix that should be used when restarting the specified server process */ @Nullable public static String getStartScriptPrefix(ProcessInfo serverProcess, ProcessInfo thisProcess) { String prefix = null; if (!OS_IS_WINDOWS) { StringBuilder buffer = new StringBuilder(); File nohup = new File(NOHUP_PATH); if (nohup.canExecute()) { buffer.append(nohup.getPath()); } File sudo = new File(SUDO_PATH); if (sudo.canExecute() && (serverProcess.getCredentials() != null) && (thisProcess.getCredentials() != null)) { long processUid = serverProcess.getCredentials().getUid(); long processGid = serverProcess.getCredentials().getGid(); long agentProcessUid = thisProcess.getCredentials().getUid(); long agentProcessGid = thisProcess.getCredentials().getGid(); boolean sudoNeededForUser = (processUid != agentProcessUid); boolean sudoNeededForGroup = (processGid != agentProcessGid); if (sudoNeededForUser || sudoNeededForGroup) { if (buffer.length() > 0) { buffer.append(' '); } buffer.append(sudo.getPath()); if (sudoNeededForUser) { buffer.append(" -u "); if (serverProcess.getCredentialsName() != null) { buffer.append(serverProcess.getCredentialsName().getUser()); } else { buffer.append(serverProcess.getCredentials().getUid()); } } if (sudoNeededForUser) { buffer.append(" -g "); if (serverProcess.getCredentialsName() != null) { buffer.append(serverProcess.getCredentialsName().getGroup()); } else { buffer.append(serverProcess.getCredentials().getGid()); } } } } if (buffer.length() > 0) { prefix = buffer.toString(); } } return prefix; } /** * Returns the list of arguments that should be passed to the start script for the specified server (e.g. JBoss AS) * process in order to start a functionally equivalent server instance. * * @param serverParentProcess the parent process of a server (e.g. JBoss AS) process * @param serverArgs the subset of arguments from the server (e.g. JBoss AS) process that should be used if the * parent process is not a script * @param optionExcludes options that should be excluded from the returned arguments if the parent process is not a * script * * @return the list of arguments that should be passed to the start script for the specified server (e.g. JBoss AS) * process in order to start a functionally equivalent server instance */ @NotNull public static List<String> getStartScriptArgs(ProcessInfo serverParentProcess, List<String> serverArgs, Set<CommandLineOption> optionExcludes) { String[] startScriptCommandLine = serverParentProcess.getCommandLine(); Integer startScriptIndex = getStartScriptIndex(startScriptCommandLine); List<String> startScriptArgs = new ArrayList<String>(); if (startScriptIndex != null) { // Skip past the script to get the arguments that were passed to the script. for (int i = (startScriptIndex + 1); i < startScriptCommandLine.length; i++) { startScriptArgs.add(startScriptCommandLine[i]); } } else { if ((optionExcludes != null) && !optionExcludes.isEmpty()) { for (int i = 0, serverArgsSize = serverArgs.size(); i < serverArgsSize; i++) { String serverArg = serverArgs.get(i); // Skip any options that the start script will take care of specifying. CommandLineOption option = null; for (CommandLineOption optionExclude : optionExcludes) { if ((optionExclude.getShortName() != null && (serverArg.equals('-' + optionExclude.getShortName()) || serverArg .startsWith('-' + optionExclude.getShortName() + "="))) || ((optionExclude.getLongName() != null) && (serverArg.equals("--" + optionExclude.getLongName()) || serverArg.startsWith("--" + optionExclude.getLongName() + "=")))) { option = optionExclude; break; } } if (option != null) { if (option.isExpectsValue() && ((i + 1) < serverArgsSize) && (((option.getShortName() != null) && serverArg.equals('-' + option.getShortName())) || (option .getLongName() != null) && serverArg.equals("--" + option.getLongName()))) { // If the option expects a value and the delimiter is a space, skip the next argument too. i++; } } else { startScriptArgs.add(serverArg); } } } else { startScriptArgs.addAll(serverArgs); } } return startScriptArgs; } /** * Returns the set of environment variables that should be passed to the start script for the specified server * (e.g. JBoss AS) process in order to start a functionally equivalent server instance. * * @param serverProcess a server (e.g. JBoss AS) process * @param serverParentProcess the parent process of the server (e.g. JBoss AS) process * @param envVarNameIncludes the names of the variables that should be included in the returned map, in addition to * a core set of OS-level variables (PATH, LD_LIBRARY_PATH, etc.) * * @return the set of environment variables that should be passed to the start script for the specified server * (e.g. JBoss AS) process */ @NotNull public static Map<String, String> getStartScriptEnv(ProcessInfo serverProcess, ProcessInfo serverParentProcess, Set<String> envVarNameIncludes) { Map<String, String> processEnvVars; if (getStartScript(serverParentProcess) != null) { processEnvVars = serverParentProcess.getEnvironmentVariables(); } else { processEnvVars = serverProcess.getEnvironmentVariables(); } List<String> fullEnvVarNameIncludes = (envVarNameIncludes != null) ? new ArrayList<String>(envVarNameIncludes) : new ArrayList<String>(); // Add the core includes at the end of the list, since end users will probably be more interested in the // app-specific env vars. fullEnvVarNameIncludes.addAll(CORE_ENV_VAR_NAME_INCLUDES); // Use a linked hashmap to maintain the order of the includes list. Map<String, String> startScriptEnv = new LinkedHashMap<String, String>(); for (String envVarName : fullEnvVarNameIncludes) { String envVarValue = processEnvVars.get(envVarName); if (envVarValue != null) { startScriptEnv.put(envVarName, envVarValue); } } // Add the fixed PATH if (File.separatorChar == '\\') { String systemRoot = processEnvVars.get("SYSTEMROOT"); systemRoot = (systemRoot == null) ? "C:\\Windows" : systemRoot; String path = systemRoot + "\\system32;" + systemRoot; startScriptEnv.put("PATH", path); } else { startScriptEnv.put("PATH", CORE_ENV_VAR_PATH_UNIX); } return startScriptEnv; } @Nullable private static Integer getStartScriptIndex(String[] serverParentProcessCommandLine) { // Assuming the specified process actually is a script, it will look something like this: // UNIX: "/bin/sh [options] ./standalone.sh --server-config=standalone-full.xml" // Windows: "cmd.exe [options] standalone.bat --server-config=standalone-full.xml" if (serverParentProcessCommandLine.length == 0 // // Observed on Solaris with 64 bit VM (-d64): parent process command line may be empty || serverParentProcessCommandLine.length == 1) { // The command line is an executable with no arguments - there's no way it's a script, so return null. return null; } int startScriptIndex; // Advance past any shell (e.g. /bin/sh or cmd.exe) options or empty args for (startScriptIndex = 1; (startScriptIndex < serverParentProcessCommandLine.length); ++startScriptIndex) { // the arg should not be null or empty, but we've seen empty args from Sigar... String arg = serverParentProcessCommandLine[startScriptIndex]; if (arg != null && !arg.isEmpty() && arg.charAt(0) != OPTION_PREFIX) { break; } } // for whatever unanticipated reason, we advanced past all of the args if (startScriptIndex == serverParentProcessCommandLine.length) { return null; } String possibleStartScript = serverParentProcessCommandLine[startScriptIndex]; return (isScript(possibleStartScript)) ? startScriptIndex : null; } private static boolean isScript(String filePath) { // TODO: What if CygWin was used to start AS7 on Windows via a shell script? return (filePath != null) && (filePath.endsWith(".sh") || filePath.matches(".*\\.((bat)|(cmd))$(?i)")); } }