package hudson.plugins.accurev;
import hudson.AbortException;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Launcher.ProcStarter;
import hudson.model.Computer;
import hudson.model.Node;
import hudson.model.TaskListener;
import hudson.plugins.accurev.parsers.output.ParseIgnoreOutput;
import hudson.plugins.accurev.parsers.output.ParseLastFewLines;
import hudson.plugins.accurev.parsers.output.ParseOutputToStream;
import hudson.util.ArgumentListBuilder;
import jenkins.model.Jenkins;
import jenkins.plugins.accurev.AccurevTool;
import org.apache.commons.lang.StringUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.UnknownHostException;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utility class that knows how to run AccuRev commands and (optionally) have
* something parse their output.
*/
public final class AccurevLauncher {
private static final Logger LOGGER = Logger.getLogger(AccurevLauncher.class.getName());
/**
* Runs a command and returns <code>true</code> if it passed,
* <code>false</code> if it failed, and logs the errors.
*
* @param humanReadableCommandName Human-readable text saying what this command is. This appears
* in the logs if there is a failure.
* @param accurevTool Which tool to find
* @param launcher Means of executing the command.
* @param machineReadableCommand The command to be executed.
* @param synchronizationLockObjectOrNull The {@link Lock} object to be used to prevent concurrent
* execution on the same machine, or <code>null</code> if no
* synchronization is required.
* @param environmentVariables The environment variables to be passed to the command.
* @param directoryToRunCommandFrom The direction that the command should be run in.
* @param listenerToLogFailuresTo One possible place to log failures, or <code>null</code>.
* @param loggerToLogFailuresTo Another place to log failures, or <code>null</code>.
* @param optionalFlagToCopyAllOutputToTaskListener Optional: If present and <code>true</code>, all command output
* will also be copied to the listener if the command is
* successful.
* @return <code>true</code> if the command succeeded.
* @throws IOException handle it above
*/
public static boolean runCommand(//
@Nonnull final String humanReadableCommandName, //
String accurevTool, @Nonnull final Launcher launcher, //
@Nonnull final ArgumentListBuilder machineReadableCommand, //
@Nullable final Lock synchronizationLockObjectOrNull, //
@Nonnull final EnvVars environmentVariables, //
@Nonnull final FilePath directoryToRunCommandFrom, //
@Nonnull final TaskListener listenerToLogFailuresTo, //
@Nonnull final Logger loggerToLogFailuresTo, //
@Nullable final boolean... optionalFlagToCopyAllOutputToTaskListener) throws IOException {
final Boolean result;
final boolean shouldLogEverything = optionalFlagToCopyAllOutputToTaskListener != null
&& optionalFlagToCopyAllOutputToTaskListener.length > 0 && optionalFlagToCopyAllOutputToTaskListener[0];
if (shouldLogEverything) {
result = runCommand(humanReadableCommandName, accurevTool, launcher,
machineReadableCommand, synchronizationLockObjectOrNull, environmentVariables,
directoryToRunCommandFrom, listenerToLogFailuresTo, loggerToLogFailuresTo,
new ParseOutputToStream(), listenerToLogFailuresTo.getLogger());
} else {
result = runCommand(humanReadableCommandName, accurevTool, launcher,
machineReadableCommand, synchronizationLockObjectOrNull, environmentVariables,
directoryToRunCommandFrom, listenerToLogFailuresTo, loggerToLogFailuresTo, new ParseIgnoreOutput(), null);
}
if (result == null) return false;
return result;
}
/**
* As
* {@link #runCommand(String, String, Launcher, ArgumentListBuilder, Lock, EnvVars, FilePath, TaskListener, Logger, ICmdOutputParser, Object)}
* but uses an {@link ICmdOutputXmlParser} instead.
*
* @param <TResult> The type of the result returned by the parser.
* @param <TContext> The type of data to be passed to the parser. Can be
* @param humanReadableCommandName Human readable command
* @param accurevTool Which tool to find
* @param launcher launcher
* @param machineReadableCommand Machine readable command
* @param synchronizationLockObjectOrNull Synchronization lock
* @param environmentVariables Environment Variables
* @param directoryToRunCommandFrom Where to run commands from
* @param listenerToLogFailuresTo logging failures to listener
* @param loggerToLogFailuresTo logging failures to logger
* @param xmlParserFactory The {@link XmlPullParserFactory} to be used to create the
* parser. If this is <code>null</code> then no command will be
* executed and the function will return <code>null</code>
* immediately.
* @param commandOutputParser Command output parser
* @param commandOutputParserContext Context of Command output parser
* @return See above.
* @throws IOException handle it above
*/
public static <TResult, TContext> TResult runCommand(//
@Nonnull final String humanReadableCommandName, //
String accurevTool, @Nonnull final Launcher launcher, //
@Nonnull final ArgumentListBuilder machineReadableCommand, //
@Nullable final Lock synchronizationLockObjectOrNull, //
@Nonnull final EnvVars environmentVariables, //
@Nonnull final FilePath directoryToRunCommandFrom, //
@Nonnull final TaskListener listenerToLogFailuresTo, //
@Nonnull final Logger loggerToLogFailuresTo, //
@Nonnull final XmlPullParserFactory xmlParserFactory, //
@Nonnull final ICmdOutputXmlParser<TResult, TContext> commandOutputParser, //
@Nullable final TContext commandOutputParserContext) throws IOException {
return runCommand(humanReadableCommandName, accurevTool, launcher,
machineReadableCommand, synchronizationLockObjectOrNull, environmentVariables,
directoryToRunCommandFrom, listenerToLogFailuresTo, loggerToLogFailuresTo, (cmdOutput, context) -> {
XmlPullParser parser = null;
try {
parser = xmlParserFactory.newPullParser();
parser.setInput(cmdOutput, null);
final TResult result = commandOutputParser.parse(parser, context);
parser.setInput(null);
parser = null;
return result;
} catch (XmlPullParserException ex) {
logCommandException(machineReadableCommand, directoryToRunCommandFrom,
humanReadableCommandName, ex, loggerToLogFailuresTo, listenerToLogFailuresTo);
return null;
} finally {
if (parser != null) {
try {
parser.setInput(null);
} catch (XmlPullParserException ex) {
logCommandException(machineReadableCommand, directoryToRunCommandFrom,
humanReadableCommandName, ex, loggerToLogFailuresTo,
listenerToLogFailuresTo);
}
cmdOutput.close();
}
}
}, commandOutputParserContext);
}
/**
* As
* {@link #runCommand(String, String, Launcher, ArgumentListBuilder, Lock, EnvVars, FilePath, TaskListener, Logger, ICmdOutputParser, Object)}
* but uses an {@link ICmdOutputXmlParser} instead.
*
* @param <TResult> The type of the result returned by the parser.
* @param <TContext> The type of data to be passed to the parser. Can be
* @param humanReadableCommandName Human readable command
* @param accurevTool Which tool to find
* @param launcher launcher
* @param machineReadableCommand Machine readable command
* @param synchronizationLockObjectOrNull Synchronization lock
* @param environmentVariables Environment Variables
* @param directoryToRunCommandFrom Where to run commands from
* @param listenerToLogFailuresTo logging failures to listener
* @param loggerToLogFailuresTo logging failures to logger
* @param xmlParserFactory The {@link XmlPullParserFactory} to be used to create the
* parser. If this is <code>null</code> then no command will be
* executed and the function will return <code>null</code>
* immediately.
* @param commandOutputParser Command output parser
* @param commandOutputParserContext Context of Command output parser
* @return See above.
* @throws IOException handle it above
*/
public static <TResult, TContext> TResult runHistCommandForAll(//
@Nonnull final String humanReadableCommandName, //
String accurevTool, @Nonnull final Launcher launcher, //
@Nonnull final ArgumentListBuilder machineReadableCommand, //
@Nullable final Lock synchronizationLockObjectOrNull, //
@Nonnull final EnvVars environmentVariables, //
@Nonnull final FilePath directoryToRunCommandFrom, //
@Nonnull final TaskListener listenerToLogFailuresTo, //
@Nonnull final Logger loggerToLogFailuresTo, //
@Nonnull final XmlPullParserFactory xmlParserFactory, //
@Nonnull final ICmdOutputXmlParser<TResult, TContext> commandOutputParser, //
@Nullable final TContext commandOutputParserContext) throws IOException {
return runCommand(humanReadableCommandName, accurevTool, launcher, machineReadableCommand,
synchronizationLockObjectOrNull, environmentVariables, directoryToRunCommandFrom,
listenerToLogFailuresTo, loggerToLogFailuresTo, (cmdOutput, context) -> {
XmlPullParser parser = null;
try {
parser = xmlParserFactory.newPullParser();
parser.setInput(cmdOutput, null);
final TResult result = commandOutputParser.parseAll(parser, context);
parser.setInput(null);
parser = null;
return result;
} catch (XmlPullParserException ex) {
logCommandException(machineReadableCommand, directoryToRunCommandFrom, humanReadableCommandName,
ex, loggerToLogFailuresTo, listenerToLogFailuresTo);
return null;
} finally {
if (parser != null) {
try {
parser.setInput(null);
} catch (XmlPullParserException ex) {
logCommandException(machineReadableCommand, directoryToRunCommandFrom,
humanReadableCommandName, ex, loggerToLogFailuresTo, listenerToLogFailuresTo);
}
cmdOutput.close();
}
}
}, commandOutputParserContext);
}
/**
* Runs a command a parses the output, returning the result of parsing that
* output. Returns <code>null</code> if the command failed or if parsing
* failed. Failures are logged.
*
* @param <TResult> The type of the result returned by the parser.
* @param <TContext> The type of data to be passed to the parser. Can be
* {@link Void} if no result is needed.
* @param humanReadableCommandName Human-readable text saying what this command is. This appears
* in the logs if there is a failure.
* @param accurevTool Which tool to find
* @param launcher Means of executing the command.
* @param machineReadableCommand The command to be executed.
* @param synchronizationLockObjectOrNull The {@link Lock} object to be used to prevent concurrent
* execution on the same machine, or <code>null</code> if no
* synchronization is required.
* @param environmentVariables The environment variables to be passed to the command.
* @param directoryToRunCommandFrom The direction that the command should be run in.
* @param listenerToLogFailuresTo One possible place to log failures, or <code>null</code>.
* @param loggerToLogFailuresTo Another place to log failures, or <code>null</code>.
* @param commandOutputParser The code that will parse the command's output (if the command
* succeeds).
* @param commandOutputParserContext Data to be passed to the parser.
* @return The data returned by the {@link ICmdOutputParser}, or
* <code>null</code> if an error occurred.
* @throws IOException handle it above
*/
public static <TResult, TContext> TResult runCommand(//
@Nonnull final String humanReadableCommandName, //
String accurevTool, @Nonnull final Launcher launcher, //
@Nonnull final ArgumentListBuilder machineReadableCommand, //
@Nullable final Lock synchronizationLockObjectOrNull, //
@Nonnull final EnvVars environmentVariables, //
@Nonnull final FilePath directoryToRunCommandFrom, //
@Nonnull final TaskListener listenerToLogFailuresTo, //
@Nonnull final Logger loggerToLogFailuresTo, //
@Nonnull final ICmdOutputParser<TResult, TContext> commandOutputParser, //
@Nullable final TContext commandOutputParserContext) throws IOException {
try (final ByteArrayStream stdout = new ByteArrayStream();
final ByteArrayStream stderr = new ByteArrayStream()) {
final OutputStream stdoutStream = stdout.getOutput();
final OutputStream stderrStream = stderr.getOutput();
final ProcStarter starter = createProcess(launcher, machineReadableCommand,
environmentVariables, directoryToRunCommandFrom, listenerToLogFailuresTo, stdoutStream, stderrStream, accurevTool);
logCommandExecution(humanReadableCommandName, machineReadableCommand, directoryToRunCommandFrom, loggerToLogFailuresTo,
listenerToLogFailuresTo);
try {
final int commandExitCode = runCommandToCompletion(starter, synchronizationLockObjectOrNull);
final InputStream outputFromCommand = stdout.getInput();
final InputStream errorFromCommand = stderr.getInput();
if (commandExitCode != 0) {
logCommandFailure(machineReadableCommand, directoryToRunCommandFrom, humanReadableCommandName,
commandExitCode, outputFromCommand, errorFromCommand, loggerToLogFailuresTo, listenerToLogFailuresTo);
return null;
}
return commandOutputParser.parse(outputFromCommand, commandOutputParserContext);
} catch (Exception ex) {
logCommandException(machineReadableCommand, directoryToRunCommandFrom, humanReadableCommandName, ex,
loggerToLogFailuresTo, listenerToLogFailuresTo);
return null;
}
} catch (InterruptedException | IOException ex) {
logCommandException(machineReadableCommand, directoryToRunCommandFrom, humanReadableCommandName, ex, loggerToLogFailuresTo, listenerToLogFailuresTo);
return null;
}
}
public static AccurevTool resolveAccurevTool(String accurevTool, TaskListener listener) {
if (StringUtils.isBlank(accurevTool)) return AccurevTool.getDefaultInstallation();
AccurevTool accurev = Jenkins.getInstance().getDescriptorByType(AccurevTool.DescriptorImpl.class).getInstallation(accurevTool);
if (accurev == null) {
listener.getLogger().println("Selected Accurev installation does not exist. Using Default");
accurev = AccurevTool.getDefaultInstallation();
}
return accurev;
}
/**
* @param accurevTool Which tool to find
* @param builtOn node where build was performed
* @param env environment variables used in the build
* @param listener build log
* @return accurev exe for builtOn node, often "Default"
*/
public static String getAccurevExe(String accurevTool, Node builtOn, EnvVars env, TaskListener listener) {
AccurevTool tool = resolveAccurevTool(accurevTool, listener);
if (builtOn != null) {
try {
tool = tool.forNode(builtOn, listener);
} catch (IOException | InterruptedException e) {
listener.getLogger().println("Failed to get accurev executable");
}
}
if (env != null) {
tool = tool.forEnvironment(env);
}
return tool.getHome();
}
private static Integer runCommandToCompletion(//
final ProcStarter starter, //
final Lock synchronizationLockObjectOrNull) throws IOException, InterruptedException {
try {
if (synchronizationLockObjectOrNull != null) {
synchronizationLockObjectOrNull.lock();
}
return starter.join(); // Exit Code from Command
} finally {
if (synchronizationLockObjectOrNull != null) {
synchronizationLockObjectOrNull.unlock();
}
}
}
private static ProcStarter createProcess(
@Nonnull final Launcher launcher,
@Nonnull final ArgumentListBuilder machineReadableCommand,
@Nonnull final EnvVars environmentVariables,
@Nonnull final FilePath directoryToRunCommandFrom,
@Nonnull TaskListener listener,
@Nonnull final OutputStream stdoutStream,
@Nonnull final OutputStream stderrStream, String accurevTool) throws IllegalStateException, IOException, InterruptedException {
String accurevPath = getAccurevExe(accurevTool, workspaceToNode(directoryToRunCommandFrom), environmentVariables, listener);
if (StringUtils.isBlank(accurevPath)) accurevPath = "accurev";
if (!accurevPath.equals(machineReadableCommand.toCommandArray()[0]))
machineReadableCommand.prepend(accurevPath);
if (!justAccurev(launcher, accurevPath)) {
throw new IllegalStateException("Cannot find accurev executable. Please check installation/tool");
}
ProcStarter starter = launcher.launch().cmds(machineReadableCommand);
Node n = workspaceToNode(directoryToRunCommandFrom);
environmentVariables.putAll(buildEnvironment(n, listener));
String path = null;
FilePath filePath = null;
if (null != n) filePath = n.getRootPath();
if (null != filePath) path = filePath.getRemote();
if (StringUtils.isNotBlank(path)) environmentVariables.putIfAbsent("ACCUREV_HOME", path);
starter = starter.envs(environmentVariables);
starter = starter.stdout(stdoutStream).stderr(stderrStream);
starter = starter.pwd(directoryToRunCommandFrom);
return starter;
}
private static void logCommandFailure(//
final ArgumentListBuilder command, //
final FilePath directoryToRunCommandFrom, //
final String commandDescription, //
final int commandExitCode, //
final InputStream commandStdoutOrNull, //
final InputStream commandStderrOrNull, //
final Logger loggerToLogFailuresTo, //
final TaskListener taskListener) throws IOException {
final String msg = commandDescription + " (" + command.toString() + ")" + " failed with exit code " + commandExitCode;
String stderr = null;
try {
stderr = getCommandErrorOutput(commandStdoutOrNull, commandStderrOrNull);
} catch (IOException ex) {
logCommandException(command, directoryToRunCommandFrom, commandDescription, ex, loggerToLogFailuresTo,
taskListener);
}
if (loggerToLogFailuresTo != null
&& (loggerToLogFailuresTo.isLoggable(Level.WARNING) || loggerToLogFailuresTo.isLoggable(Level.INFO))) {
final String hostname = getRemoteHostname(directoryToRunCommandFrom);
loggerToLogFailuresTo.warning(hostname + ": " + msg);
if (stderr != null) {
loggerToLogFailuresTo.info(hostname + ": " + stderr);
}
}
if (taskListener != null) {
if (stderr != null) {
taskListener.fatalError(stderr);
}
taskListener.fatalError(msg);
}
}
private static String getCommandErrorOutput(final InputStream commandStdoutOrNull,
final InputStream commandStderrOrNull) throws IOException {
final StringBuilder outputText = new StringBuilder();
if (commandStdoutOrNull != null) parseCommandOutput(commandStdoutOrNull, 10, outputText);
if (commandStderrOrNull != null) parseCommandOutput(commandStderrOrNull, 5, outputText);
if (outputText.length() > 0) {
return outputText.toString();
} else {
return null;
}
}
private static void parseCommandOutput(final InputStream commandOutput,
final Integer maxNumberOfLines,
final StringBuilder outputText) throws IOException {
final String newLine = System.getProperty("line.separator");
final ParseLastFewLines tailParser = new ParseLastFewLines();
final List<String> outputLines = tailParser.parse(commandOutput, maxNumberOfLines);
for (final String line : outputLines) {
if (outputText.length() > 0) {
outputText.append(newLine);
}
outputText.append(line);
}
}
private static void logCommandException(//
final ArgumentListBuilder command, //
final FilePath directoryToRunCommandFrom, //
final String commandDescription, //
final Throwable exception, //
final Logger loggerToLogFailuresTo, //
final TaskListener taskListener) throws IOException {
final String hostname = getRemoteHostname(directoryToRunCommandFrom);
final String msg = hostname + ": " + commandDescription + " (" + command.toString() + ")"
+ " failed with " + exception.toString();
logException(msg, exception, loggerToLogFailuresTo, taskListener);
}
static void logException(//
final String summary, //
final Throwable exception, //
final Logger logger, //
final TaskListener taskListener) throws IOException {
if (logger != null) {
logger.log(Level.SEVERE, summary, exception);
}
if (taskListener != null) {
taskListener.fatalError(summary);
exception.printStackTrace(taskListener.getLogger());
throw new AbortException(exception.getMessage());
}
}
private static void logCommandExecution(//
final String commandDescription,
final ArgumentListBuilder command, //
final FilePath directoryToRunCommandFrom, //
final Logger loggerToLogFailuresTo, //
final TaskListener taskListener) {
if (loggerToLogFailuresTo != null && loggerToLogFailuresTo.isLoggable(Level.FINE)) {
final String hostname = getRemoteHostname(directoryToRunCommandFrom);
final String msg = hostname + ": " + command.toString();
loggerToLogFailuresTo.log(Level.FINE, msg);
}
}
private static String getRemoteHostname(final FilePath directoryToRunCommandFrom) {
try {
final RemoteWorkspaceDetails act = directoryToRunCommandFrom.act(new DetermineRemoteHostname("."));
return act.getHostName();
} catch (UnknownHostException e) {
return "Unable to determine actual hostname, ensure proper FQDN.\n" + e.toString();
} catch (IOException | InterruptedException e) {
return e.toString();
}
}
public static EnvVars buildEnvironment(Node node, TaskListener listener) throws IOException, InterruptedException {
EnvVars env;
if (null != node) {
final Computer computer = node.toComputer();
env = (computer != null) ? computer.buildEnvironment(listener) : new EnvVars();
} else {
env = new EnvVars();
}
// servlet container may have set CLASSPATH in its launch script,
// so don't let that inherit to the new child process.
env.put("CLASSPATH", "");
return env;
}
private static boolean justAccurev(Launcher launcher, String exe) {
try {
return launcher.launch().quiet(true).cmdAsSingleString(exe).join() == 0;
} catch (IOException | InterruptedException e1) {
return false;
}
}
@CheckForNull
public static Node workspaceToNode(FilePath workspace) {
Computer computer = workspace.toComputer();
Node node = null;
if (null != computer) node = computer.getNode();
return null != node ? node : Jenkins.getInstance();
}
/**
* Interface implemented by code that interprets the output of AccuRev
* commands.
* <p>
* Intended to separate out the running of commands from the actual parsing
* of their results in an attempt to reduce code duplication.
*
* @param <TResult> The output of the parsing process.
* @param <TContext> Context object that will be passed to the parser each time it
* is called.
*/
public interface ICmdOutputParser<TResult, TContext> {
/**
* Parses the command's output.
*
* @param cmdOutput The stream that contains the output of the command.
* @param context Context passed in when the command was run.
* @return The result of the parsing.
* @throws UnhandledAccurevCommandOutput if the command output was invalid.
* @throws IOException on failing IO
*/
TResult parse(InputStream cmdOutput, TContext context) throws UnhandledAccurevCommandOutput, IOException;
}
/**
* Interface implemented by code that interprets the output of AccuRev
* commands.
* <p>
* Intended to separate out the running of commands from the actual parsing
* of their results in an attempt to reduce code duplication.
*
* @param <TResult> The output of the parsing process.
* @param <TContext> Context object that will be passed to the parser each time it
* is called.
*/
public interface ICmdOutputXmlParser<TResult, TContext> {
/**
* Parses the command's output.
*
* @param parser The {@link XmlPullParser} that contains the output of the
* command.
* @param context Context passed in when the command was run.
* @return The result of the parsing.
* @throws UnhandledAccurevCommandOutput if the command output was invalid.
* @throws IOException on failing IO
* @throws XmlPullParserException if failed to Parse
*/
TResult parse(XmlPullParser parser, TContext context) throws UnhandledAccurevCommandOutput, IOException, XmlPullParserException;
/**
* Parses all the transactions from the command's output.
*
* @param parser The {@link XmlPullParser} that contains the output of the
* command.
* @param context Context passed in when the command was run.
* @return The result of the parsing.
* @throws IOException on failing IO
* @throws XmlPullParserException if failed to Parse
*/
default TResult parseAll(XmlPullParser parser, TContext context) throws IOException, XmlPullParserException {
return null;
}
}
/**
* Exception that can be throw if the AccuRev command's output cannot be
* parsed or is otherwise invalid.
*/
public static final class UnhandledAccurevCommandOutput extends Exception {
public UnhandledAccurevCommandOutput(String message, Throwable cause) {
super(message, cause);
}
public UnhandledAccurevCommandOutput(String message) {
super(message);
}
public UnhandledAccurevCommandOutput(Throwable cause) {
super(cause);
}
}
}